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]
|
args: [--line-length=100, --target-version=py311]
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- 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:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--line-length=100, --target-version=py311]
|
args: [--line-length=100, --target-version=py311]
|
||||||
|
|||||||
@@ -117,3 +117,11 @@ target-version = ['py311']
|
|||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = "py311"
|
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 asyncio
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
_server_thread: Optional[threading.Thread] = None
|
_server_thread: threading.Thread | None = None
|
||||||
_server: Optional[Any] = None # uvicorn.Server
|
_server: Any | None = None # uvicorn.Server
|
||||||
_loop: Optional[asyncio.AbstractEventLoop] = None
|
_loop: asyncio.AbstractEventLoop | None = None
|
||||||
|
|
||||||
|
|
||||||
def start_server(data_dir: str, port: int = 8080) -> None:
|
def start_server(data_dir: str, port: int = 8080) -> None:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Optional
|
from typing import Callable
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from starlette.websockets import WebSocket
|
from starlette.websockets import WebSocket
|
||||||
@@ -61,8 +61,8 @@ async def stream_capture_test(
|
|||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
engine_factory: Callable,
|
engine_factory: Callable,
|
||||||
duration: float,
|
duration: float,
|
||||||
pp_filters: Optional[list] = None,
|
pp_filters: list | None = None,
|
||||||
preview_width: Optional[int] = None,
|
preview_width: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run a capture test, streaming intermediate thumbnails and a final full-res frame.
|
"""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."""
|
"""Audio source routes: CRUD for audio sources + real-time test WebSocket."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
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"])
|
@router.get("/api/v1/audio-sources", response_model=AudioSourceListResponse, tags=["Audio Sources"])
|
||||||
async def list_audio_sources(
|
async def list_audio_sources(
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
source_type: Optional[str] = Query(
|
source_type: str | None = Query(
|
||||||
None, description="Filter by source_type: capture or processed"
|
None, description="Filter by source_type: capture or processed"
|
||||||
),
|
),
|
||||||
store: AudioSourceStore = Depends(get_audio_source_store),
|
store: AudioSourceStore = Depends(get_audio_source_store),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Output target routes: CRUD endpoints and batch state/metrics queries."""
|
"""Output target routes: CRUD endpoints and batch state/metrics queries."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, HTTPException, Depends
|
from fastapi import APIRouter, Body, HTTPException, Depends
|
||||||
|
|
||||||
@@ -421,7 +421,7 @@ async def get_target(
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_effective_color_vs_id(
|
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:
|
) -> str:
|
||||||
if payload_id is not None:
|
if payload_id is not None:
|
||||||
return payload_id
|
return payload_id
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -190,7 +189,7 @@ async def list_all_tags(_: AuthRequired):
|
|||||||
@router.get("/api/v1/config/displays", response_model=DisplayListResponse, tags=["Config"])
|
@router.get("/api/v1/config/displays", response_model=DisplayListResponse, tags=["Config"])
|
||||||
async def get_displays(
|
async def get_displays(
|
||||||
_: AuthRequired,
|
_: 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.
|
"""Get list of available displays.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Value source routes: CRUD for value sources."""
|
"""Value source routes: CRUD for value sources."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
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"])
|
@router.get("/api/v1/value-sources", response_model=ValueSourceListResponse, tags=["Value Sources"])
|
||||||
async def list_value_sources(
|
async def list_value_sources(
|
||||||
_auth: AuthRequired,
|
_auth: AuthRequired,
|
||||||
source_type: Optional[str] = Query(
|
source_type: str | None = Query(
|
||||||
None,
|
None,
|
||||||
description="Filter by source_type: static, animated, audio, adaptive_time, or adaptive_scene",
|
description="Filter by source_type: static, animated, audio, adaptive_time, or adaptive_scene",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Asset schemas (CRUD)."""
|
"""Asset schemas (CRUD)."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -9,15 +9,15 @@ from pydantic import BaseModel, Field
|
|||||||
class AssetUpdate(BaseModel):
|
class AssetUpdate(BaseModel):
|
||||||
"""Request to update asset metadata."""
|
"""Request to update asset metadata."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, min_length=1, max_length=100, description="Display name")
|
name: str | None = Field(None, min_length=1, max_length=100, description="Display name")
|
||||||
description: Optional[str] = Field(None, max_length=500, description="Optional description")
|
description: str | None = Field(None, max_length=500, description="Optional description")
|
||||||
tags: Optional[List[str]] = Field(None, description="User-defined tags")
|
tags: List[str] | None = Field(None, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
mime_type: str = Field(description="MIME type")
|
||||||
asset_type: str = Field(description="Asset type: sound, image, video, other")
|
asset_type: str = Field(description="Asset type: sound, image, video, other")
|
||||||
size_bytes: int = Field(description="File size in bytes")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
prebuilt: bool = Field(False, description="Whether this is a shipped prebuilt asset")
|
prebuilt: bool = Field(False, description="Whether this is a shipped prebuilt asset")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Audio processing template schemas."""
|
"""Audio processing template schemas."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -15,14 +15,14 @@ class AudioProcessingTemplateCreate(BaseModel):
|
|||||||
filters: List[FilterInstanceSchema] = Field(
|
filters: List[FilterInstanceSchema] = Field(
|
||||||
default_factory=list, description="Ordered list of audio filter instances"
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -32,18 +32,18 @@ class AudioProcessingTemplateCreate(BaseModel):
|
|||||||
class AudioProcessingTemplateUpdate(BaseModel):
|
class AudioProcessingTemplateUpdate(BaseModel):
|
||||||
"""Request to update an audio processing template."""
|
"""Request to update an audio processing template."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||||
filters: Optional[List[FilterInstanceSchema]] = Field(
|
filters: List[FilterInstanceSchema] | None = Field(
|
||||||
None, description="Ordered list of audio filter instances"
|
None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
description: str | None = Field(None, description="Template description")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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."""
|
"""Audio source schemas — discriminated unions per source type."""
|
||||||
|
|
||||||
from datetime import datetime
|
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
|
from pydantic import BaseModel, Discriminator, Field, Tag
|
||||||
|
|
||||||
@@ -15,16 +15,16 @@ class _AudioSourceResponseBase(BaseModel):
|
|||||||
|
|
||||||
id: str = Field(description="Source ID")
|
id: str = Field(description="Source ID")
|
||||||
name: str = Field(description="Source name")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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"
|
source_type: Literal["capture"] = "capture"
|
||||||
device_index: int = Field(description="Audio device index (-1 = default)")
|
device_index: int = Field(description="Audio device index (-1 = default)")
|
||||||
is_loopback: bool = Field(description="WASAPI loopback mode")
|
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):
|
class ProcessedAudioSourceResponse(_AudioSourceResponseBase):
|
||||||
@@ -45,10 +45,8 @@ class ProcessedAudioSourceResponse(_AudioSourceResponseBase):
|
|||||||
|
|
||||||
|
|
||||||
AudioSourceResponse = Annotated[
|
AudioSourceResponse = Annotated[
|
||||||
Union[
|
Annotated[CaptureAudioSourceResponse, Tag("capture")]
|
||||||
Annotated[CaptureAudioSourceResponse, Tag("capture")],
|
| Annotated[ProcessedAudioSourceResponse, Tag("processed")],
|
||||||
Annotated[ProcessedAudioSourceResponse, Tag("processed")],
|
|
||||||
],
|
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -61,14 +59,14 @@ class _AudioSourceCreateBase(BaseModel):
|
|||||||
"""Shared fields for all audio source create requests."""
|
"""Shared fields for all audio source create requests."""
|
||||||
|
|
||||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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"
|
source_type: Literal["capture"] = "capture"
|
||||||
device_index: int = Field(-1, description="Audio device index (-1 = default)")
|
device_index: int = Field(-1, description="Audio device index (-1 = default)")
|
||||||
is_loopback: bool = Field(True, description="True for system audio (WASAPI loopback)")
|
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):
|
class ProcessedAudioSourceCreate(_AudioSourceCreateBase):
|
||||||
@@ -89,10 +87,8 @@ class ProcessedAudioSourceCreate(_AudioSourceCreateBase):
|
|||||||
|
|
||||||
|
|
||||||
AudioSourceCreate = Annotated[
|
AudioSourceCreate = Annotated[
|
||||||
Union[
|
Annotated[CaptureAudioSourceCreate, Tag("capture")]
|
||||||
Annotated[CaptureAudioSourceCreate, Tag("capture")],
|
| Annotated[ProcessedAudioSourceCreate, Tag("processed")],
|
||||||
Annotated[ProcessedAudioSourceCreate, Tag("processed")],
|
|
||||||
],
|
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -104,15 +100,15 @@ AudioSourceCreate = Annotated[
|
|||||||
class _AudioSourceUpdateBase(BaseModel):
|
class _AudioSourceUpdateBase(BaseModel):
|
||||||
"""Shared fields for all audio source update requests."""
|
"""Shared fields for all audio source update requests."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
name: str | None = Field(None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -121,24 +117,22 @@ class _AudioSourceUpdateBase(BaseModel):
|
|||||||
|
|
||||||
class CaptureAudioSourceUpdate(_AudioSourceUpdateBase):
|
class CaptureAudioSourceUpdate(_AudioSourceUpdateBase):
|
||||||
source_type: Literal["capture"] = "capture"
|
source_type: Literal["capture"] = "capture"
|
||||||
device_index: Optional[int] = Field(None, description="Audio device index (-1 = default)")
|
device_index: int | None = Field(None, description="Audio device index (-1 = default)")
|
||||||
is_loopback: Optional[bool] = Field(None, description="True for system audio (WASAPI loopback)")
|
is_loopback: bool | None = Field(None, 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 ProcessedAudioSourceUpdate(_AudioSourceUpdateBase):
|
class ProcessedAudioSourceUpdate(_AudioSourceUpdateBase):
|
||||||
source_type: Literal["processed"] = "processed"
|
source_type: Literal["processed"] = "processed"
|
||||||
audio_source_id: Optional[str] = Field(None, description="Input audio source ID")
|
audio_source_id: str | None = Field(None, description="Input audio source ID")
|
||||||
audio_processing_template_id: Optional[str] = Field(
|
audio_processing_template_id: str | None = Field(
|
||||||
None, description="Audio processing template ID"
|
None, description="Audio processing template ID"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
AudioSourceUpdate = Annotated[
|
AudioSourceUpdate = Annotated[
|
||||||
Union[
|
Annotated[CaptureAudioSourceUpdate, Tag("capture")]
|
||||||
Annotated[CaptureAudioSourceUpdate, Tag("capture")],
|
| Annotated[ProcessedAudioSourceUpdate, Tag("processed")],
|
||||||
Annotated[ProcessedAudioSourceUpdate, Tag("processed")],
|
|
||||||
],
|
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Audio capture template and engine schemas."""
|
"""Audio capture template and engine schemas."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -14,14 +14,14 @@ class AudioTemplateCreate(BaseModel):
|
|||||||
description="Audio engine type (e.g., 'wasapi', 'sounddevice')", min_length=1
|
description="Audio engine type (e.g., 'wasapi', 'sounddevice')", min_length=1
|
||||||
)
|
)
|
||||||
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -31,17 +31,17 @@ class AudioTemplateCreate(BaseModel):
|
|||||||
class AudioTemplateUpdate(BaseModel):
|
class AudioTemplateUpdate(BaseModel):
|
||||||
"""Request to update an audio template."""
|
"""Request to update an audio template."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||||
engine_type: Optional[str] = Field(None, description="Audio engine type")
|
engine_type: str | None = Field(None, description="Audio engine type")
|
||||||
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
engine_config: Dict | None = Field(None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
description: str | None = Field(None, description="Template description")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Automation-related schemas."""
|
"""Automation-related schemas."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -11,57 +11,53 @@ class RuleSchema(BaseModel):
|
|||||||
|
|
||||||
rule_type: str = Field(description="Rule type discriminator (e.g. 'application')")
|
rule_type: str = Field(description="Rule type discriminator (e.g. 'application')")
|
||||||
# Application rule fields
|
# Application rule fields
|
||||||
apps: Optional[List[str]] = Field(None, description="Process names (for application rule)")
|
apps: List[str] | None = Field(None, description="Process names (for application rule)")
|
||||||
match_type: Optional[str] = Field(
|
match_type: str | None = Field(
|
||||||
None, description="'running' or 'topmost' (for application rule)"
|
None, description="'running' or 'topmost' (for application rule)"
|
||||||
)
|
)
|
||||||
# Time-of-day rule fields
|
# Time-of-day rule fields
|
||||||
start_time: Optional[str] = Field(None, description="Start 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: Optional[str] = Field(None, description="End 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
|
# System idle rule fields
|
||||||
idle_minutes: Optional[int] = Field(
|
idle_minutes: int | None = Field(
|
||||||
None, description="Idle timeout in minutes (for system_idle rule)"
|
None, description="Idle timeout in minutes (for system_idle rule)"
|
||||||
)
|
)
|
||||||
when_idle: Optional[bool] = Field(
|
when_idle: bool | None = Field(None, description="True=active when idle (for system_idle rule)")
|
||||||
None, description="True=active when idle (for system_idle rule)"
|
|
||||||
)
|
|
||||||
# Display state rule fields
|
# 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 rule fields
|
||||||
mqtt_source_id: Optional[str] = Field(None, description="MQTT source ID (for mqtt rule)")
|
mqtt_source_id: str | None = Field(None, description="MQTT source ID (for mqtt rule)")
|
||||||
topic: Optional[str] = Field(None, description="MQTT topic to watch (for mqtt rule)")
|
topic: str | None = Field(None, description="MQTT topic to watch (for mqtt rule)")
|
||||||
payload: Optional[str] = Field(None, description="Expected payload value (for mqtt rule)")
|
payload: str | None = Field(None, description="Expected payload value (for mqtt rule)")
|
||||||
match_mode: Optional[str] = Field(
|
match_mode: str | None = Field(
|
||||||
None, description="'exact', 'contains', or 'regex' (for mqtt rule)"
|
None, description="'exact', 'contains', or 'regex' (for mqtt rule)"
|
||||||
)
|
)
|
||||||
# Webhook rule fields
|
# Webhook rule fields
|
||||||
token: Optional[str] = Field(
|
token: str | None = Field(None, description="Secret token for webhook URL (for webhook rule)")
|
||||||
None, description="Secret token for webhook URL (for webhook rule)"
|
|
||||||
)
|
|
||||||
# Home Assistant rule fields
|
# 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)"
|
None, description="Home Assistant source ID (for home_assistant rule)"
|
||||||
)
|
)
|
||||||
entity_id: Optional[str] = Field(
|
entity_id: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description="HA entity ID, e.g. 'binary_sensor.front_door' (for home_assistant rule)",
|
description="HA entity ID, e.g. 'binary_sensor.front_door' (for home_assistant rule)",
|
||||||
)
|
)
|
||||||
# HTTP poll rule fields
|
# HTTP poll rule fields
|
||||||
value_source_id: Optional[str] = Field(
|
value_source_id: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description=(
|
description=(
|
||||||
"Value source ID (for http_poll rule). The referenced "
|
"Value source ID (for http_poll rule). The referenced "
|
||||||
"ValueSource must be of source_type='http'."
|
"ValueSource must be of source_type='http'."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
operator: Optional[str] = Field(
|
operator: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description=(
|
description=(
|
||||||
"Comparison operator for http_poll rule: "
|
"Comparison operator for http_poll rule: "
|
||||||
"'equals', 'not_equals', 'contains', 'regex', 'gt', 'lt', 'exists'."
|
"'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')"
|
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")
|
enabled: bool = Field(default=True, description="Whether the automation is enabled")
|
||||||
rule_logic: str = Field(default="or", description="How rules combine: 'or' or 'and'")
|
rule_logic: str = Field(default="or", description="How rules combine: 'or' or 'and'")
|
||||||
rules: List[RuleSchema] = Field(default_factory=list, description="List of rules")
|
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(
|
deactivation_mode: str = Field(
|
||||||
default="none", description="'none', 'revert', or 'fallback_scene'"
|
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"
|
None, description="Scene preset for fallback deactivation"
|
||||||
)
|
)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -100,24 +96,22 @@ class AutomationCreate(BaseModel):
|
|||||||
class AutomationUpdate(BaseModel):
|
class AutomationUpdate(BaseModel):
|
||||||
"""Request to update an automation."""
|
"""Request to update an automation."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Automation name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Automation name", min_length=1, max_length=100)
|
||||||
enabled: Optional[bool] = Field(None, description="Whether the automation is enabled")
|
enabled: bool | None = Field(None, description="Whether the automation is enabled")
|
||||||
rule_logic: Optional[str] = Field(None, description="How rules combine: 'or' or 'and'")
|
rule_logic: str | None = Field(None, description="How rules combine: 'or' or 'and'")
|
||||||
rules: Optional[List[RuleSchema]] = Field(None, description="List of rules")
|
rules: List[RuleSchema] | None = Field(None, 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: Optional[str] = Field(
|
deactivation_mode: str | None = Field(None, description="'none', 'revert', or 'fallback_scene'")
|
||||||
None, description="'none', 'revert', or 'fallback_scene'"
|
deactivation_scene_preset_id: str | None = Field(
|
||||||
)
|
|
||||||
deactivation_scene_preset_id: Optional[str] = Field(
|
|
||||||
None, description="Scene preset for fallback deactivation"
|
None, description="Scene preset for fallback deactivation"
|
||||||
)
|
)
|
||||||
tags: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
enabled: bool = Field(description="Whether the automation is enabled")
|
||||||
rule_logic: str = Field(description="Rule combination logic")
|
rule_logic: str = Field(description="Rule combination logic")
|
||||||
rules: List[RuleSchema] = Field(description="List of rules")
|
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_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")
|
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)"
|
None, description="Webhook URL for the first webhook rule (if any)"
|
||||||
)
|
)
|
||||||
is_active: bool = Field(default=False, description="Whether the automation is currently active")
|
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"
|
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"
|
None, description="Last time this automation was deactivated"
|
||||||
)
|
)
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Color strip processing template schemas."""
|
"""Color strip processing template schemas."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -15,14 +15,14 @@ class ColorStripProcessingTemplateCreate(BaseModel):
|
|||||||
filters: List[FilterInstanceSchema] = Field(
|
filters: List[FilterInstanceSchema] = Field(
|
||||||
default_factory=list, description="Ordered list of filter instances"
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -32,18 +32,18 @@ class ColorStripProcessingTemplateCreate(BaseModel):
|
|||||||
class ColorStripProcessingTemplateUpdate(BaseModel):
|
class ColorStripProcessingTemplateUpdate(BaseModel):
|
||||||
"""Request to update a color strip processing template."""
|
"""Request to update a color strip processing template."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||||
filters: Optional[List[FilterInstanceSchema]] = Field(
|
filters: List[FilterInstanceSchema] | None = Field(
|
||||||
None, description="Ordered list of filter instances"
|
None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
description: str | None = Field(None, description="Template description")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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."""
|
"""Color strip source schemas — discriminated unions per source type."""
|
||||||
|
|
||||||
from datetime import datetime
|
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 pydantic import BaseModel, Discriminator, Field, Tag, model_validator
|
||||||
|
|
||||||
from ledgrab.api.schemas.devices import Calibration
|
from ledgrab.api.schemas.devices import Calibration
|
||||||
|
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
# Helper models (unchanged)
|
# Helper models (unchanged)
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
@@ -16,10 +15,10 @@ from ledgrab.api.schemas.devices import Calibration
|
|||||||
class AppSoundOverride(BaseModel):
|
class AppSoundOverride(BaseModel):
|
||||||
"""Per-application sound override for notification sources."""
|
"""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)"
|
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)"
|
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
|
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: 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,
|
None,
|
||||||
description="Optional right-side RGB color for a hard edge (bidirectional stop)",
|
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")
|
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")
|
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"
|
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"
|
None, description="Optional color strip processing template ID"
|
||||||
)
|
)
|
||||||
start: int = Field(default=0, ge=0, description="First LED index for range (0 = full strip)")
|
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")
|
id: str = Field(description="Source ID")
|
||||||
name: str = Field(description="Source name")
|
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)")
|
led_count: int = Field(0, description="Total LED count (0 = auto)")
|
||||||
overlay_active: bool = Field(
|
overlay_active: bool = Field(
|
||||||
False, description="Whether the screen overlay is currently active"
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
picture_source_id: str = Field(description="Picture source ID")
|
||||||
smoothing: Any = Field(description="Temporal smoothing")
|
smoothing: Any = Field(description="Temporal smoothing")
|
||||||
interpolation_mode: str = Field(description="Interpolation mode")
|
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):
|
class PictureAdvancedCSSResponse(_CSSResponseBase):
|
||||||
source_type: Literal["picture_advanced"] = "picture_advanced"
|
source_type: Literal["picture_advanced"] = "picture_advanced"
|
||||||
smoothing: Any = Field(description="Temporal smoothing")
|
smoothing: Any = Field(description="Temporal smoothing")
|
||||||
interpolation_mode: str = Field(description="Interpolation mode")
|
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):
|
class SingleColorCSSResponse(_CSSResponseBase):
|
||||||
source_type: Literal["single_color"] = "single_color"
|
source_type: Literal["single_color"] = "single_color"
|
||||||
color: Any = Field(description="Solid RGB 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):
|
class GradientCSSResponse(_CSSResponseBase):
|
||||||
source_type: Literal["gradient"] = "gradient"
|
source_type: Literal["gradient"] = "gradient"
|
||||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops")
|
stops: List[ColorStop] | None = Field(None, description="Color stops")
|
||||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
|
animation: AnimationConfig | None = Field(None, description="Procedural animation config")
|
||||||
easing: str = Field(description="Gradient interpolation easing")
|
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):
|
class EffectCSSResponse(_CSSResponseBase):
|
||||||
source_type: Literal["effect"] = "effect"
|
source_type: Literal["effect"] = "effect"
|
||||||
effect_type: str = Field(description="Effect algorithm")
|
effect_type: str = Field(description="Effect algorithm")
|
||||||
palette: str = Field(description="Named palette")
|
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: Any = Field(description="Primary color")
|
||||||
intensity: Any = Field(description="Effect intensity")
|
intensity: Any = Field(description="Effect intensity")
|
||||||
scale: Any = Field(description="Spatial scale")
|
scale: Any = Field(description="Spatial scale")
|
||||||
mirror: bool = Field(description="Mirror/bounce mode")
|
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):
|
class CompositeCSSResponse(_CSSResponseBase):
|
||||||
@@ -165,7 +164,7 @@ class AudioCSSResponse(_CSSResponseBase):
|
|||||||
sensitivity: Any = Field(description="Audio sensitivity")
|
sensitivity: Any = Field(description="Audio sensitivity")
|
||||||
smoothing: Any = Field(description="Temporal smoothing")
|
smoothing: Any = Field(description="Temporal smoothing")
|
||||||
palette: str = Field(description="Named palette")
|
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: Any = Field(description="Primary color")
|
||||||
color_peak: Any = Field(description="Peak color")
|
color_peak: Any = Field(description="Peak color")
|
||||||
mirror: bool = Field(description="Mirror mode")
|
mirror: bool = Field(description="Mirror mode")
|
||||||
@@ -188,7 +187,7 @@ class NotificationCSSResponse(_CSSResponseBase):
|
|||||||
app_filter_mode: str = Field(description="App filter mode")
|
app_filter_mode: str = Field(description="App filter mode")
|
||||||
app_filter_list: List[str] = Field(default_factory=list, description="App names for filter")
|
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")
|
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")
|
sound_volume: Any = Field(description="Global notification sound volume")
|
||||||
app_sounds: Dict[str, dict] = Field(default_factory=dict, description="Per-app sound overrides")
|
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"
|
source_type: Literal["math_wave"] = "math_wave"
|
||||||
waves: List[dict] = Field(description="Wave layer definitions")
|
waves: List[dict] = Field(description="Wave layer definitions")
|
||||||
speed: Any = Field(description="Global speed multiplier (bindable)")
|
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):
|
class GameEventCSSResponse(_CSSResponseBase):
|
||||||
@@ -248,25 +247,23 @@ class GameEventCSSResponse(_CSSResponseBase):
|
|||||||
|
|
||||||
|
|
||||||
ColorStripSourceResponse = Annotated[
|
ColorStripSourceResponse = Annotated[
|
||||||
Union[
|
Annotated[PictureCSSResponse, Tag("picture")]
|
||||||
Annotated[PictureCSSResponse, Tag("picture")],
|
| Annotated[PictureAdvancedCSSResponse, Tag("picture_advanced")]
|
||||||
Annotated[PictureAdvancedCSSResponse, Tag("picture_advanced")],
|
| Annotated[SingleColorCSSResponse, Tag("single_color")]
|
||||||
Annotated[SingleColorCSSResponse, Tag("single_color")],
|
| Annotated[GradientCSSResponse, Tag("gradient")]
|
||||||
Annotated[GradientCSSResponse, Tag("gradient")],
|
| Annotated[EffectCSSResponse, Tag("effect")]
|
||||||
Annotated[EffectCSSResponse, Tag("effect")],
|
| Annotated[CompositeCSSResponse, Tag("composite")]
|
||||||
Annotated[CompositeCSSResponse, Tag("composite")],
|
| Annotated[MappedCSSResponse, Tag("mapped")]
|
||||||
Annotated[MappedCSSResponse, Tag("mapped")],
|
| Annotated[AudioCSSResponse, Tag("audio")]
|
||||||
Annotated[AudioCSSResponse, Tag("audio")],
|
| Annotated[ApiInputCSSResponse, Tag("api_input")]
|
||||||
Annotated[ApiInputCSSResponse, Tag("api_input")],
|
| Annotated[NotificationCSSResponse, Tag("notification")]
|
||||||
Annotated[NotificationCSSResponse, Tag("notification")],
|
| Annotated[DaylightCSSResponse, Tag("daylight")]
|
||||||
Annotated[DaylightCSSResponse, Tag("daylight")],
|
| Annotated[CandlelightCSSResponse, Tag("candlelight")]
|
||||||
Annotated[CandlelightCSSResponse, Tag("candlelight")],
|
| Annotated[ProcessedCSSResponse, Tag("processed")]
|
||||||
Annotated[ProcessedCSSResponse, Tag("processed")],
|
| Annotated[WeatherCSSResponse, Tag("weather")]
|
||||||
Annotated[WeatherCSSResponse, Tag("weather")],
|
| Annotated[KeyColorsCSSResponse, Tag("key_colors")]
|
||||||
Annotated[KeyColorsCSSResponse, Tag("key_colors")],
|
| Annotated[MathWaveCSSResponse, Tag("math_wave")]
|
||||||
Annotated[MathWaveCSSResponse, Tag("math_wave")],
|
| Annotated[GameEventCSSResponse, Tag("game_event")],
|
||||||
Annotated[GameEventCSSResponse, Tag("game_event")],
|
|
||||||
],
|
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -281,15 +278,15 @@ class _CSSCreateBase(BaseModel):
|
|||||||
|
|
||||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
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)
|
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)
|
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||||
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
picture_source_id: str = Field(default="", description="Picture source ID")
|
||||||
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
||||||
interpolation_mode: str = Field(default="average", description="Interpolation mode")
|
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):
|
class PictureAdvancedCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["picture_advanced"] = "picture_advanced"
|
source_type: Literal["picture_advanced"] = "picture_advanced"
|
||||||
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
||||||
interpolation_mode: str = Field(default="average", description="Interpolation mode")
|
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):
|
class SingleColorCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["single_color"] = "single_color"
|
source_type: Literal["single_color"] = "single_color"
|
||||||
color: Any = Field(default=None, description="Solid RGB color [R,G,B]")
|
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):
|
class GradientCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["gradient"] = "gradient"
|
source_type: Literal["gradient"] = "gradient"
|
||||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops")
|
stops: List[ColorStop] | None = Field(None, description="Color stops")
|
||||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
|
animation: AnimationConfig | None = Field(None, description="Procedural animation config")
|
||||||
easing: Optional[str] = Field(None, description="Gradient easing")
|
easing: str | None = Field(None, description="Gradient easing")
|
||||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||||
|
|
||||||
|
|
||||||
class EffectCSSCreate(_CSSCreateBase):
|
class EffectCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["effect"] = "effect"
|
source_type: Literal["effect"] = "effect"
|
||||||
effect_type: Optional[str] = Field(None, description="Effect algorithm")
|
effect_type: str | None = Field(None, description="Effect algorithm")
|
||||||
palette: Optional[str] = Field(None, description="Named palette")
|
palette: str | None = Field(None, 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(default=None, description="Primary color")
|
color: Any = Field(default=None, description="Primary color")
|
||||||
intensity: Any = Field(default=None, description="Effect intensity (0.1-2.0)")
|
intensity: Any = Field(default=None, description="Effect intensity (0.1-2.0)")
|
||||||
scale: Any = Field(default=None, description="Spatial scale (0.5-5.0)")
|
scale: Any = Field(default=None, description="Spatial scale (0.5-5.0)")
|
||||||
mirror: Optional[bool] = Field(None, description="Mirror/bounce mode")
|
mirror: bool | None = Field(None, 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 CompositeCSSCreate(_CSSCreateBase):
|
class CompositeCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["composite"] = "composite"
|
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):
|
class MappedCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["mapped"] = "mapped"
|
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):
|
class AudioCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["audio"] = "audio"
|
source_type: Literal["audio"] = "audio"
|
||||||
visualization_mode: Optional[str] = Field(None, description="Audio visualization mode")
|
visualization_mode: str | None = Field(None, description="Audio visualization mode")
|
||||||
audio_source_id: Optional[str] = Field(None, description="Mono audio source ID")
|
audio_source_id: str | None = Field(None, description="Mono audio source ID")
|
||||||
sensitivity: Any = Field(default=None, description="Audio sensitivity (0.1-5.0)")
|
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)")
|
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
||||||
palette: Optional[str] = Field(None, description="Named palette")
|
palette: str | None = Field(None, 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(default=None, description="Primary color")
|
color: Any = Field(default=None, description="Primary color")
|
||||||
color_peak: Any = Field(default=None, description="Peak color [R,G,B]")
|
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(
|
beat_decay: Any = Field(
|
||||||
default=None, description="Beat pulse decay rate (music modes, 0.01-0.5)"
|
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"
|
source_type: Literal["api_input"] = "api_input"
|
||||||
fallback_color: Any = Field(default=None, description="Fallback RGB color [R,G,B]")
|
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)")
|
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):
|
class NotificationCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["notification"] = "notification"
|
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")
|
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"
|
None, description="Default color"
|
||||||
)
|
)
|
||||||
app_colors: Optional[Dict[str, str]] = Field(None, description="Per-app hex colors")
|
app_colors: Dict[str, str] | None = Field(None, description="Per-app hex colors")
|
||||||
app_filter_mode: Optional[str] = Field(None, description="App filter mode")
|
app_filter_mode: str | None = Field(None, description="App filter mode")
|
||||||
app_filter_list: Optional[List[str]] = Field(None, description="App names for filter")
|
app_filter_list: List[str] | None = Field(None, description="App names for filter")
|
||||||
os_listener: Optional[bool] = Field(None, description="Listen for OS notifications")
|
os_listener: bool | None = Field(None, description="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(default=None, description="Global notification sound volume")
|
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"
|
None, description="Per-app sound overrides"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -391,9 +388,9 @@ class NotificationCSSCreate(_CSSCreateBase):
|
|||||||
class DaylightCSSCreate(_CSSCreateBase):
|
class DaylightCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["daylight"] = "daylight"
|
source_type: Literal["daylight"] = "daylight"
|
||||||
speed: Any = Field(default=None, description="Cycle speed multiplier (0.1-10.0)")
|
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")
|
use_real_time: bool | None = Field(None, description="Use wall-clock time")
|
||||||
latitude: Optional[float] = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
|
latitude: float | None = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
|
||||||
longitude: Optional[float] = Field(
|
longitude: float | None = Field(
|
||||||
None, description="Longitude (-180 to 180)", ge=-180.0, le=180.0
|
None, description="Longitude (-180 to 180)", ge=-180.0, le=180.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -402,23 +399,23 @@ class CandlelightCSSCreate(_CSSCreateBase):
|
|||||||
source_type: Literal["candlelight"] = "candlelight"
|
source_type: Literal["candlelight"] = "candlelight"
|
||||||
color: Any = Field(default=None, description="Candle color [R,G,B]")
|
color: Any = Field(default=None, description="Candle color [R,G,B]")
|
||||||
intensity: Any = Field(default=None, description="Candle intensity (0.1-2.0)")
|
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
|
None, description="Number of candle sources (1-20)", ge=1, le=20
|
||||||
)
|
)
|
||||||
speed: Any = Field(default=None, description="Flicker speed (0.1-10.0)")
|
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)")
|
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):
|
class ProcessedCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["processed"] = "processed"
|
source_type: Literal["processed"] = "processed"
|
||||||
input_source_id: Optional[str] = Field(None, description="Input color strip source ID")
|
input_source_id: str | None = Field(None, description="Input color strip source ID")
|
||||||
processing_template_id: Optional[str] = Field(None, description="Processing template ID")
|
processing_template_id: str | None = Field(None, description="Processing template ID")
|
||||||
|
|
||||||
|
|
||||||
class WeatherCSSCreate(_CSSCreateBase):
|
class WeatherCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["weather"] = "weather"
|
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)")
|
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)")
|
temperature_influence: Any = Field(default=None, description="Temperature influence (0.0-1.0)")
|
||||||
|
|
||||||
@@ -426,49 +423,47 @@ class WeatherCSSCreate(_CSSCreateBase):
|
|||||||
class KeyColorsCSSCreate(_CSSCreateBase):
|
class KeyColorsCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["key_colors"] = "key_colors"
|
source_type: Literal["key_colors"] = "key_colors"
|
||||||
picture_source_id: str = Field(default="", description="Picture source ID")
|
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")
|
interpolation_mode: str = Field(default="average", description="Interpolation mode")
|
||||||
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
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: 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"
|
None, description="Dynamic brightness value source ID"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MathWaveCSSCreate(_CSSCreateBase):
|
class MathWaveCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["math_wave"] = "math_wave"
|
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)")
|
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):
|
class GameEventCSSCreate(_CSSCreateBase):
|
||||||
source_type: Literal["game_event"] = "game_event"
|
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)")
|
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[
|
ColorStripSourceCreate = Annotated[
|
||||||
Union[
|
Annotated[PictureCSSCreate, Tag("picture")]
|
||||||
Annotated[PictureCSSCreate, Tag("picture")],
|
| Annotated[PictureAdvancedCSSCreate, Tag("picture_advanced")]
|
||||||
Annotated[PictureAdvancedCSSCreate, Tag("picture_advanced")],
|
| Annotated[SingleColorCSSCreate, Tag("single_color")]
|
||||||
Annotated[SingleColorCSSCreate, Tag("single_color")],
|
| Annotated[GradientCSSCreate, Tag("gradient")]
|
||||||
Annotated[GradientCSSCreate, Tag("gradient")],
|
| Annotated[EffectCSSCreate, Tag("effect")]
|
||||||
Annotated[EffectCSSCreate, Tag("effect")],
|
| Annotated[CompositeCSSCreate, Tag("composite")]
|
||||||
Annotated[CompositeCSSCreate, Tag("composite")],
|
| Annotated[MappedCSSCreate, Tag("mapped")]
|
||||||
Annotated[MappedCSSCreate, Tag("mapped")],
|
| Annotated[AudioCSSCreate, Tag("audio")]
|
||||||
Annotated[AudioCSSCreate, Tag("audio")],
|
| Annotated[ApiInputCSSCreate, Tag("api_input")]
|
||||||
Annotated[ApiInputCSSCreate, Tag("api_input")],
|
| Annotated[NotificationCSSCreate, Tag("notification")]
|
||||||
Annotated[NotificationCSSCreate, Tag("notification")],
|
| Annotated[DaylightCSSCreate, Tag("daylight")]
|
||||||
Annotated[DaylightCSSCreate, Tag("daylight")],
|
| Annotated[CandlelightCSSCreate, Tag("candlelight")]
|
||||||
Annotated[CandlelightCSSCreate, Tag("candlelight")],
|
| Annotated[ProcessedCSSCreate, Tag("processed")]
|
||||||
Annotated[ProcessedCSSCreate, Tag("processed")],
|
| Annotated[WeatherCSSCreate, Tag("weather")]
|
||||||
Annotated[WeatherCSSCreate, Tag("weather")],
|
| Annotated[KeyColorsCSSCreate, Tag("key_colors")]
|
||||||
Annotated[KeyColorsCSSCreate, Tag("key_colors")],
|
| Annotated[MathWaveCSSCreate, Tag("math_wave")]
|
||||||
Annotated[MathWaveCSSCreate, Tag("math_wave")],
|
| Annotated[GameEventCSSCreate, Tag("game_event")],
|
||||||
Annotated[GameEventCSSCreate, Tag("game_event")],
|
|
||||||
],
|
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -481,17 +476,17 @@ ColorStripSourceCreate = Annotated[
|
|||||||
class _CSSUpdateBase(BaseModel):
|
class _CSSUpdateBase(BaseModel):
|
||||||
"""Shared fields for all color strip source update requests."""
|
"""Shared fields for all color strip source update requests."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
name: str | None = 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)
|
led_count: int | None = Field(None, description="Total LED count (0 = auto)", ge=0)
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||||
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
clock_id: str | None = Field(None, description="Optional sync clock ID")
|
||||||
tags: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -500,66 +495,66 @@ class _CSSUpdateBase(BaseModel):
|
|||||||
|
|
||||||
class PictureCSSUpdate(_CSSUpdateBase):
|
class PictureCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["picture"] = "picture"
|
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)")
|
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
||||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
|
interpolation_mode: str | None = Field(None, description="Interpolation mode")
|
||||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
calibration: Calibration | None = Field(None, description="LED calibration")
|
||||||
|
|
||||||
|
|
||||||
class PictureAdvancedCSSUpdate(_CSSUpdateBase):
|
class PictureAdvancedCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["picture_advanced"] = "picture_advanced"
|
source_type: Literal["picture_advanced"] = "picture_advanced"
|
||||||
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
||||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
|
interpolation_mode: str | None = Field(None, description="Interpolation mode")
|
||||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
calibration: Calibration | None = Field(None, description="LED calibration")
|
||||||
|
|
||||||
|
|
||||||
class SingleColorCSSUpdate(_CSSUpdateBase):
|
class SingleColorCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["single_color"] = "single_color"
|
source_type: Literal["single_color"] = "single_color"
|
||||||
color: Any = Field(default=None, description="Solid RGB color [R,G,B]")
|
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):
|
class GradientCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["gradient"] = "gradient"
|
source_type: Literal["gradient"] = "gradient"
|
||||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops")
|
stops: List[ColorStop] | None = Field(None, description="Color stops")
|
||||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
|
animation: AnimationConfig | None = Field(None, description="Procedural animation config")
|
||||||
easing: Optional[str] = Field(None, description="Gradient easing")
|
easing: str | None = Field(None, description="Gradient easing")
|
||||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||||
|
|
||||||
|
|
||||||
class EffectCSSUpdate(_CSSUpdateBase):
|
class EffectCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["effect"] = "effect"
|
source_type: Literal["effect"] = "effect"
|
||||||
effect_type: Optional[str] = Field(None, description="Effect algorithm")
|
effect_type: str | None = Field(None, description="Effect algorithm")
|
||||||
palette: Optional[str] = Field(None, description="Named palette")
|
palette: str | None = Field(None, 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(default=None, description="Primary color")
|
color: Any = Field(default=None, description="Primary color")
|
||||||
intensity: Any = Field(default=None, description="Effect intensity (0.1-2.0)")
|
intensity: Any = Field(default=None, description="Effect intensity (0.1-2.0)")
|
||||||
scale: Any = Field(default=None, description="Spatial scale (0.5-5.0)")
|
scale: Any = Field(default=None, description="Spatial scale (0.5-5.0)")
|
||||||
mirror: Optional[bool] = Field(None, description="Mirror/bounce mode")
|
mirror: bool | None = Field(None, 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 CompositeCSSUpdate(_CSSUpdateBase):
|
class CompositeCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["composite"] = "composite"
|
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):
|
class MappedCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["mapped"] = "mapped"
|
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):
|
class AudioCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["audio"] = "audio"
|
source_type: Literal["audio"] = "audio"
|
||||||
visualization_mode: Optional[str] = Field(None, description="Audio visualization mode")
|
visualization_mode: str | None = Field(None, description="Audio visualization mode")
|
||||||
audio_source_id: Optional[str] = Field(None, description="Mono audio source ID")
|
audio_source_id: str | None = Field(None, description="Mono audio source ID")
|
||||||
sensitivity: Any = Field(default=None, description="Audio sensitivity (0.1-5.0)")
|
sensitivity: Any = Field(default=None, description="Audio sensitivity (0.1-5.0)")
|
||||||
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
||||||
palette: Optional[str] = Field(None, description="Named palette")
|
palette: str | None = Field(None, 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(default=None, description="Primary color")
|
color: Any = Field(default=None, description="Primary color")
|
||||||
color_peak: Any = Field(default=None, description="Peak color [R,G,B]")
|
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)")
|
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"
|
source_type: Literal["api_input"] = "api_input"
|
||||||
fallback_color: Any = Field(default=None, description="Fallback RGB color [R,G,B]")
|
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)")
|
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):
|
class NotificationCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["notification"] = "notification"
|
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")
|
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"
|
None, description="Default color"
|
||||||
)
|
)
|
||||||
app_colors: Optional[Dict[str, str]] = Field(None, description="Per-app hex colors")
|
app_colors: Dict[str, str] | None = Field(None, description="Per-app hex colors")
|
||||||
app_filter_mode: Optional[str] = Field(None, description="App filter mode")
|
app_filter_mode: str | None = Field(None, description="App filter mode")
|
||||||
app_filter_list: Optional[List[str]] = Field(None, description="App names for filter")
|
app_filter_list: List[str] | None = Field(None, description="App names for filter")
|
||||||
os_listener: Optional[bool] = Field(None, description="Listen for OS notifications")
|
os_listener: bool | None = Field(None, description="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(default=None, description="Global notification sound volume")
|
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"
|
None, description="Per-app sound overrides"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -591,9 +586,9 @@ class NotificationCSSUpdate(_CSSUpdateBase):
|
|||||||
class DaylightCSSUpdate(_CSSUpdateBase):
|
class DaylightCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["daylight"] = "daylight"
|
source_type: Literal["daylight"] = "daylight"
|
||||||
speed: Any = Field(default=None, description="Cycle speed multiplier (0.1-10.0)")
|
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")
|
use_real_time: bool | None = Field(None, description="Use wall-clock time")
|
||||||
latitude: Optional[float] = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
|
latitude: float | None = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
|
||||||
longitude: Optional[float] = Field(
|
longitude: float | None = Field(
|
||||||
None, description="Longitude (-180 to 180)", ge=-180.0, le=180.0
|
None, description="Longitude (-180 to 180)", ge=-180.0, le=180.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -602,73 +597,71 @@ class CandlelightCSSUpdate(_CSSUpdateBase):
|
|||||||
source_type: Literal["candlelight"] = "candlelight"
|
source_type: Literal["candlelight"] = "candlelight"
|
||||||
color: Any = Field(default=None, description="Candle color [R,G,B]")
|
color: Any = Field(default=None, description="Candle color [R,G,B]")
|
||||||
intensity: Any = Field(default=None, description="Candle intensity (0.1-2.0)")
|
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
|
None, description="Number of candle sources (1-20)", ge=1, le=20
|
||||||
)
|
)
|
||||||
speed: Any = Field(default=None, description="Flicker speed (0.1-10.0)")
|
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)")
|
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):
|
class ProcessedCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["processed"] = "processed"
|
source_type: Literal["processed"] = "processed"
|
||||||
input_source_id: Optional[str] = Field(None, description="Input color strip source ID")
|
input_source_id: str | None = Field(None, description="Input color strip source ID")
|
||||||
processing_template_id: Optional[str] = Field(None, description="Processing template ID")
|
processing_template_id: str | None = Field(None, description="Processing template ID")
|
||||||
|
|
||||||
|
|
||||||
class WeatherCSSUpdate(_CSSUpdateBase):
|
class WeatherCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["weather"] = "weather"
|
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)")
|
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)")
|
temperature_influence: Any = Field(default=None, description="Temperature influence (0.0-1.0)")
|
||||||
|
|
||||||
|
|
||||||
class KeyColorsCSSUpdate(_CSSUpdateBase):
|
class KeyColorsCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["key_colors"] = "key_colors"
|
source_type: Literal["key_colors"] = "key_colors"
|
||||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
picture_source_id: str | None = Field(None, 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: Optional[str] = Field(None, description="Interpolation mode")
|
interpolation_mode: str | None = Field(None, description="Interpolation mode")
|
||||||
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
||||||
brightness: Any = Field(default=None, description="Brightness (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"
|
None, description="Dynamic brightness value source ID"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MathWaveCSSUpdate(_CSSUpdateBase):
|
class MathWaveCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["math_wave"] = "math_wave"
|
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)")
|
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):
|
class GameEventCSSUpdate(_CSSUpdateBase):
|
||||||
source_type: Literal["game_event"] = "game_event"
|
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)")
|
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[
|
ColorStripSourceUpdate = Annotated[
|
||||||
Union[
|
Annotated[PictureCSSUpdate, Tag("picture")]
|
||||||
Annotated[PictureCSSUpdate, Tag("picture")],
|
| Annotated[PictureAdvancedCSSUpdate, Tag("picture_advanced")]
|
||||||
Annotated[PictureAdvancedCSSUpdate, Tag("picture_advanced")],
|
| Annotated[SingleColorCSSUpdate, Tag("single_color")]
|
||||||
Annotated[SingleColorCSSUpdate, Tag("single_color")],
|
| Annotated[GradientCSSUpdate, Tag("gradient")]
|
||||||
Annotated[GradientCSSUpdate, Tag("gradient")],
|
| Annotated[EffectCSSUpdate, Tag("effect")]
|
||||||
Annotated[EffectCSSUpdate, Tag("effect")],
|
| Annotated[CompositeCSSUpdate, Tag("composite")]
|
||||||
Annotated[CompositeCSSUpdate, Tag("composite")],
|
| Annotated[MappedCSSUpdate, Tag("mapped")]
|
||||||
Annotated[MappedCSSUpdate, Tag("mapped")],
|
| Annotated[AudioCSSUpdate, Tag("audio")]
|
||||||
Annotated[AudioCSSUpdate, Tag("audio")],
|
| Annotated[ApiInputCSSUpdate, Tag("api_input")]
|
||||||
Annotated[ApiInputCSSUpdate, Tag("api_input")],
|
| Annotated[NotificationCSSUpdate, Tag("notification")]
|
||||||
Annotated[NotificationCSSUpdate, Tag("notification")],
|
| Annotated[DaylightCSSUpdate, Tag("daylight")]
|
||||||
Annotated[DaylightCSSUpdate, Tag("daylight")],
|
| Annotated[CandlelightCSSUpdate, Tag("candlelight")]
|
||||||
Annotated[CandlelightCSSUpdate, Tag("candlelight")],
|
| Annotated[ProcessedCSSUpdate, Tag("processed")]
|
||||||
Annotated[ProcessedCSSUpdate, Tag("processed")],
|
| Annotated[WeatherCSSUpdate, Tag("weather")]
|
||||||
Annotated[WeatherCSSUpdate, Tag("weather")],
|
| Annotated[KeyColorsCSSUpdate, Tag("key_colors")]
|
||||||
Annotated[KeyColorsCSSUpdate, Tag("key_colors")],
|
| Annotated[MathWaveCSSUpdate, Tag("math_wave")]
|
||||||
Annotated[MathWaveCSSUpdate, Tag("math_wave")],
|
| Annotated[GameEventCSSUpdate, Tag("game_event")],
|
||||||
Annotated[GameEventCSSUpdate, Tag("game_event")],
|
|
||||||
],
|
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -699,17 +692,17 @@ class SegmentPayload(BaseModel):
|
|||||||
``color`` therefore fills the entire strip.
|
``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)"
|
None, ge=0, description="Starting LED index (default 0 = beginning of strip)"
|
||||||
)
|
)
|
||||||
length: Optional[int] = Field(
|
length: int | None = Field(
|
||||||
None,
|
None,
|
||||||
ge=1,
|
ge=1,
|
||||||
description="Number of LEDs in segment (default = led_count - start)",
|
description="Number of LEDs in segment (default = led_count - start)",
|
||||||
)
|
)
|
||||||
mode: Literal["solid", "per_pixel", "gradient"] = Field(description="Fill mode")
|
mode: Literal["solid", "per_pixel", "gradient"] = Field(description="Fill mode")
|
||||||
color: Optional[List[int]] = Field(None, description="RGB for solid mode [R,G,B]")
|
color: List[int] | None = Field(None, description="RGB for solid mode [R,G,B]")
|
||||||
colors: Optional[List[List[int]]] = Field(
|
colors: List[List[int]] | None = Field(
|
||||||
None, description="Colors for per_pixel/gradient [[R,G,B],...]"
|
None, description="Colors for per_pixel/gradient [[R,G,B],...]"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -742,12 +735,10 @@ class ColorPushRequest(BaseModel):
|
|||||||
At least one must be provided.
|
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)"
|
None, description="LED color array [[R,G,B], ...] (0-255 each)"
|
||||||
)
|
)
|
||||||
segments: Optional[List[SegmentPayload]] = Field(
|
segments: List[SegmentPayload] | None = Field(None, description="Segment-based color updates")
|
||||||
None, description="Segment-based color updates"
|
|
||||||
)
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def _require_colors_or_segments(self) -> "ColorPushRequest":
|
def _require_colors_or_segments(self) -> "ColorPushRequest":
|
||||||
@@ -759,8 +750,8 @@ class ColorPushRequest(BaseModel):
|
|||||||
class NotifyRequest(BaseModel):
|
class NotifyRequest(BaseModel):
|
||||||
"""Request to trigger a notification on a notification color strip source."""
|
"""Request to trigger a notification on a notification color strip source."""
|
||||||
|
|
||||||
app: Optional[str] = Field(None, description="App name for color lookup")
|
app: str | None = Field(None, description="App name for color lookup")
|
||||||
color: Optional[str] = Field(None, description="Hex color override (#RRGGBB)")
|
color: str | None = Field(None, description="Hex color override (#RRGGBB)")
|
||||||
|
|
||||||
|
|
||||||
class CSSCalibrationTestRequest(BaseModel):
|
class CSSCalibrationTestRequest(BaseModel):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Shared schemas used across multiple route modules."""
|
"""Shared schemas used across multiple route modules."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Optional
|
from typing import Dict
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ class ErrorResponse(BaseModel):
|
|||||||
|
|
||||||
error: str = Field(description="Error type")
|
error: str = Field(description="Error type")
|
||||||
message: str = Field(description="Error message")
|
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")
|
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Error timestamp")
|
||||||
|
|
||||||
|
|
||||||
@@ -19,11 +19,11 @@ class CaptureImage(BaseModel):
|
|||||||
"""Captured image with metadata."""
|
"""Captured image with metadata."""
|
||||||
|
|
||||||
image: str = Field(description="Base64-encoded thumbnail image data")
|
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")
|
width: int = Field(description="Original image width in pixels")
|
||||||
height: int = Field(description="Original image height in pixels")
|
height: int = Field(description="Original image height in pixels")
|
||||||
thumbnail_width: Optional[int] = Field(None, description="Thumbnail width (if resized)")
|
thumbnail_width: int | None = Field(None, description="Thumbnail width (if resized)")
|
||||||
thumbnail_height: Optional[int] = Field(None, description="Thumbnail height (if resized)")
|
thumbnail_height: int | None = Field(None, description="Thumbnail height (if resized)")
|
||||||
|
|
||||||
|
|
||||||
class BorderExtraction(BaseModel):
|
class BorderExtraction(BaseModel):
|
||||||
@@ -48,7 +48,7 @@ class TemplateTestResponse(BaseModel):
|
|||||||
"""Response from template test."""
|
"""Response from template test."""
|
||||||
|
|
||||||
full_capture: CaptureImage = Field(description="Full screen capture with thumbnail")
|
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)"
|
None, description="Extracted border images (deprecated)"
|
||||||
)
|
)
|
||||||
performance: PerformanceMetrics = Field(description="Performance metrics")
|
performance: PerformanceMetrics = Field(description="Performance metrics")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Device-related schemas (CRUD, calibration, device state)."""
|
"""Device-related schemas (CRUD, calibration, device state)."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Literal, Optional
|
from typing import Dict, List, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -10,149 +10,145 @@ class DeviceCreate(BaseModel):
|
|||||||
"""Request to create/attach an LED device."""
|
"""Request to create/attach an LED device."""
|
||||||
|
|
||||||
name: str = Field(description="Device name", min_length=1, max_length=100)
|
name: str = Field(description="Device name", min_length=1, max_length=100)
|
||||||
url: Optional[str] = Field(
|
url: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description="Device URL (e.g., http://192.168.1.100 or COM3). Not required for group devices.",
|
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)")
|
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)"
|
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)")
|
baud_rate: int | None = Field(None, description="Serial baud rate (for adalight devices)")
|
||||||
auto_shutdown: Optional[bool] = Field(
|
auto_shutdown: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Turn off device when server stops (defaults to true for adalight)",
|
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)"
|
None, ge=0, le=5000, description="Simulated send latency in ms (mock devices)"
|
||||||
)
|
)
|
||||||
rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)")
|
rgbw: bool | None = Field(None, description="RGBW mode (mock devices)")
|
||||||
zone_mode: Optional[str] = Field(None, description="OpenRGB zone mode: combined or separate")
|
zone_mode: str | None = Field(None, description="OpenRGB zone mode: combined or separate")
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
# DMX (Art-Net / sACN) fields
|
# DMX (Art-Net / sACN) fields
|
||||||
dmx_protocol: Optional[str] = Field(None, description="DMX protocol: artnet or sacn")
|
dmx_protocol: str | None = Field(None, description="DMX protocol: artnet or sacn")
|
||||||
dmx_start_universe: Optional[int] = Field(
|
dmx_start_universe: int | None = Field(None, ge=0, le=32767, description="DMX start universe")
|
||||||
None, ge=0, le=32767, description="DMX start universe"
|
dmx_start_channel: int | None = Field(
|
||||||
)
|
|
||||||
dmx_start_channel: Optional[int] = Field(
|
|
||||||
None, ge=1, le=512, description="DMX start channel (1-512)"
|
None, ge=1, le=512, description="DMX start channel (1-512)"
|
||||||
)
|
)
|
||||||
# DDP fields
|
# 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)"
|
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)"
|
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,
|
None,
|
||||||
ge=0,
|
ge=0,
|
||||||
le=5,
|
le=5,
|
||||||
description="DDP color order: 0=GRB 1=RGB 2=BRG 3=RBG 4=BGR 5=GBR (most receivers expect RGB)",
|
description="DDP color order: 0=GRB 1=RGB 2=BRG 3=RBG 4=BGR 5=GBR (most receivers expect RGB)",
|
||||||
)
|
)
|
||||||
# ESP-NOW fields
|
# 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)"
|
None, description="ESP-NOW peer MAC address (e.g. AA:BB:CC:DD:EE:FF)"
|
||||||
)
|
)
|
||||||
espnow_channel: Optional[int] = Field(
|
espnow_channel: int | None = Field(None, ge=1, le=14, description="ESP-NOW WiFi channel (1-14)")
|
||||||
None, ge=1, le=14, description="ESP-NOW WiFi channel (1-14)"
|
|
||||||
)
|
|
||||||
# Philips Hue fields
|
# Philips Hue fields
|
||||||
hue_username: Optional[str] = Field(None, description="Hue bridge username (from pairing)")
|
hue_username: str | None = Field(None, description="Hue bridge username (from pairing)")
|
||||||
hue_client_key: Optional[str] = Field(None, description="Hue entertainment client key (hex)")
|
hue_client_key: str | None = Field(None, description="Hue entertainment client key (hex)")
|
||||||
hue_entertainment_group_id: Optional[str] = Field(
|
hue_entertainment_group_id: str | None = Field(
|
||||||
None, description="Hue entertainment group/zone ID"
|
None, description="Hue entertainment group/zone ID"
|
||||||
)
|
)
|
||||||
# Yeelight fields
|
# Yeelight fields
|
||||||
yeelight_min_interval_ms: Optional[int] = Field(
|
yeelight_min_interval_ms: int | None = Field(
|
||||||
None,
|
None,
|
||||||
ge=0,
|
ge=0,
|
||||||
le=10000,
|
le=10000,
|
||||||
description="Yeelight client-side rate limit between commands in ms (default 500)",
|
description="Yeelight client-side rate limit between commands in ms (default 500)",
|
||||||
)
|
)
|
||||||
# WiZ fields
|
# WiZ fields
|
||||||
wiz_min_interval_ms: Optional[int] = Field(
|
wiz_min_interval_ms: int | None = Field(
|
||||||
None,
|
None,
|
||||||
ge=0,
|
ge=0,
|
||||||
le=10000,
|
le=10000,
|
||||||
description="WiZ client-side rate limit between commands in ms (default 50)",
|
description="WiZ client-side rate limit between commands in ms (default 50)",
|
||||||
)
|
)
|
||||||
# LIFX fields
|
# LIFX fields
|
||||||
lifx_min_interval_ms: Optional[int] = Field(
|
lifx_min_interval_ms: int | None = Field(
|
||||||
None,
|
None,
|
||||||
ge=0,
|
ge=0,
|
||||||
le=10000,
|
le=10000,
|
||||||
description="LIFX client-side rate limit between commands in ms (default 50)",
|
description="LIFX client-side rate limit between commands in ms (default 50)",
|
||||||
)
|
)
|
||||||
# Govee fields
|
# Govee fields
|
||||||
govee_min_interval_ms: Optional[int] = Field(
|
govee_min_interval_ms: int | None = Field(
|
||||||
None,
|
None,
|
||||||
ge=0,
|
ge=0,
|
||||||
le=10000,
|
le=10000,
|
||||||
description="Govee client-side rate limit between commands in ms (default 50)",
|
description="Govee client-side rate limit between commands in ms (default 50)",
|
||||||
)
|
)
|
||||||
# OPC fields
|
# OPC fields
|
||||||
opc_channel: Optional[int] = Field(
|
opc_channel: int | None = Field(
|
||||||
None,
|
None,
|
||||||
ge=0,
|
ge=0,
|
||||||
le=255,
|
le=255,
|
||||||
description="OPC channel (0 = broadcast to all channels on the server)",
|
description="OPC channel (0 = broadcast to all channels on the server)",
|
||||||
)
|
)
|
||||||
# Nanoleaf fields
|
# Nanoleaf fields
|
||||||
nanoleaf_token: Optional[str] = Field(
|
nanoleaf_token: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=512,
|
max_length=512,
|
||||||
description="Nanoleaf auth token returned by the pairing handshake",
|
description="Nanoleaf auth token returned by the pairing handshake",
|
||||||
)
|
)
|
||||||
nanoleaf_min_interval_ms: Optional[int] = Field(
|
nanoleaf_min_interval_ms: int | None = Field(
|
||||||
None,
|
None,
|
||||||
ge=0,
|
ge=0,
|
||||||
le=10000,
|
le=10000,
|
||||||
description="Nanoleaf client-side rate limit between commands in ms (default 100)",
|
description="Nanoleaf client-side rate limit between commands in ms (default 100)",
|
||||||
)
|
)
|
||||||
# SPI Direct fields
|
# 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"
|
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"
|
None, description="LED chipset: WS2812, WS2812B, WS2811, SK6812, SK6812_RGBW"
|
||||||
)
|
)
|
||||||
# Razer Chroma fields
|
# Razer Chroma fields
|
||||||
chroma_device_type: Optional[str] = Field(
|
chroma_device_type: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description="Chroma peripheral type: keyboard, mouse, mousepad, headset, chromalink, keypad",
|
description="Chroma peripheral type: keyboard, mouse, mousepad, headset, chromalink, keypad",
|
||||||
)
|
)
|
||||||
# SteelSeries GameSense fields
|
# 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"
|
None, description="GameSense device type: keyboard, mouse, headset, mousepad, indicator"
|
||||||
)
|
)
|
||||||
# BLE controller fields
|
# BLE controller fields
|
||||||
ble_family: Optional[str] = Field(
|
ble_family: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description="BLE protocol family: sp110e, triones, zengge, govee",
|
description="BLE protocol family: sp110e, triones, zengge, govee",
|
||||||
)
|
)
|
||||||
ble_govee_key: Optional[str] = Field(
|
ble_govee_key: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description="Govee AES key (hex) — required for encrypted Govee firmware",
|
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"
|
None, description="Default color strip processing template ID"
|
||||||
)
|
)
|
||||||
# Group device fields
|
# 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)"
|
None, description="Ordered list of child device IDs (for group device type)"
|
||||||
)
|
)
|
||||||
group_mode: Optional[str] = Field(
|
group_mode: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description="Group mode: sequence (LEDs concatenated) or independent (each child gets full strip resampled)",
|
description="Group mode: sequence (LEDs concatenated) or independent (each child gets full strip resampled)",
|
||||||
)
|
)
|
||||||
# Custom card icon (frontend display only)
|
# Custom card icon (frontend display only)
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library (e.g. 'mouse', 'motherboard'). Empty/null hides the plate.",
|
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,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the card's channel accent.",
|
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):
|
class DeviceUpdate(BaseModel):
|
||||||
"""Request to update device information."""
|
"""Request to update device information."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Device name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Device name", min_length=1, max_length=100)
|
||||||
url: Optional[str] = Field(None, description="Device URL or serial port")
|
url: str | None = Field(None, description="Device URL or serial port")
|
||||||
enabled: Optional[bool] = Field(None, description="Whether device is enabled")
|
enabled: bool | None = Field(None, description="Whether device is enabled")
|
||||||
led_count: Optional[int] = Field(
|
led_count: int | None = Field(
|
||||||
None,
|
None,
|
||||||
ge=1,
|
ge=1,
|
||||||
le=10000,
|
le=10000,
|
||||||
description="Number of LEDs (for devices with manual_led_count capability)",
|
description="Number of LEDs (for devices with manual_led_count capability)",
|
||||||
)
|
)
|
||||||
baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)")
|
baud_rate: int | None = Field(None, description="Serial baud rate (for adalight devices)")
|
||||||
auto_shutdown: Optional[bool] = Field(None, description="Turn off device when server stops")
|
auto_shutdown: bool | None = Field(None, description="Turn off device when server stops")
|
||||||
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)"
|
None, ge=0, le=5000, description="Simulated send latency in ms (mock devices)"
|
||||||
)
|
)
|
||||||
rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)")
|
rgbw: bool | None = Field(None, description="RGBW mode (mock devices)")
|
||||||
zone_mode: Optional[str] = Field(None, description="OpenRGB zone mode: combined or separate")
|
zone_mode: str | None = Field(None, description="OpenRGB zone mode: combined or separate")
|
||||||
tags: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
dmx_protocol: Optional[str] = Field(None, description="DMX protocol: artnet or sacn")
|
dmx_protocol: str | None = Field(None, description="DMX protocol: artnet or sacn")
|
||||||
dmx_start_universe: Optional[int] = Field(
|
dmx_start_universe: int | None = Field(None, ge=0, le=32767, description="DMX start universe")
|
||||||
None, ge=0, le=32767, description="DMX start universe"
|
dmx_start_channel: int | None = Field(
|
||||||
)
|
|
||||||
dmx_start_channel: Optional[int] = Field(
|
|
||||||
None, ge=1, le=512, description="DMX start channel (1-512)"
|
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)"
|
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_destination_id: int | None = 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")
|
ddp_color_order: int | None = 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_peer_mac: str | None = Field(None, description="ESP-NOW peer MAC address")
|
||||||
espnow_channel: Optional[int] = Field(None, ge=1, le=14, description="ESP-NOW WiFi channel")
|
espnow_channel: int | None = Field(None, ge=1, le=14, description="ESP-NOW WiFi channel")
|
||||||
hue_username: Optional[str] = Field(None, description="Hue bridge username")
|
hue_username: str | None = Field(None, description="Hue bridge username")
|
||||||
hue_client_key: Optional[str] = Field(None, description="Hue entertainment client key")
|
hue_client_key: str | None = Field(None, description="Hue entertainment client key")
|
||||||
hue_entertainment_group_id: Optional[str] = Field(
|
hue_entertainment_group_id: str | None = Field(None, description="Hue entertainment group ID")
|
||||||
None, description="Hue entertainment group ID"
|
yeelight_min_interval_ms: int | None = Field(
|
||||||
)
|
|
||||||
yeelight_min_interval_ms: Optional[int] = Field(
|
|
||||||
None, ge=0, le=10000, description="Yeelight client-side rate limit in ms"
|
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"
|
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"
|
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"
|
None, ge=0, le=10000, description="Govee client-side rate limit in ms"
|
||||||
)
|
)
|
||||||
opc_channel: Optional[int] = Field(
|
opc_channel: int | None = Field(None, ge=0, le=255, description="OPC channel (0 = broadcast)")
|
||||||
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(
|
||||||
nanoleaf_token: Optional[str] = Field(None, max_length=512, description="Nanoleaf auth token")
|
|
||||||
nanoleaf_min_interval_ms: Optional[int] = Field(
|
|
||||||
None, ge=0, le=10000, description="Nanoleaf client-side rate limit in ms"
|
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_speed_hz: int | None = Field(None, ge=100000, le=4000000, description="SPI clock speed")
|
||||||
spi_led_type: Optional[str] = Field(None, description="LED chipset type")
|
spi_led_type: str | None = Field(None, description="LED chipset type")
|
||||||
chroma_device_type: Optional[str] = Field(None, description="Chroma peripheral type")
|
chroma_device_type: str | None = Field(None, description="Chroma peripheral type")
|
||||||
gamesense_device_type: Optional[str] = Field(None, description="GameSense device type")
|
gamesense_device_type: str | None = Field(None, description="GameSense device type")
|
||||||
ble_family: Optional[str] = Field(
|
ble_family: str | None = Field(
|
||||||
None, description="BLE protocol family: sp110e, triones, zengge, govee"
|
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"
|
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"
|
None, description="Default color strip processing template ID"
|
||||||
)
|
)
|
||||||
# Group device fields
|
# 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)"
|
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
|
# Custom card icon
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
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)",
|
description="Calibration mode: simple (4-edge) or advanced (multi-source lines)",
|
||||||
)
|
)
|
||||||
# Advanced mode: ordered list of 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)"
|
default=None, description="Line list for advanced mode (ignored in simple mode)"
|
||||||
)
|
)
|
||||||
# Simple mode fields
|
# Simple mode fields
|
||||||
@@ -388,7 +378,7 @@ class DeviceResponse(BaseModel):
|
|||||||
device_type: str = Field(default="wled", description="LED device type")
|
device_type: str = Field(default="wled", description="LED device type")
|
||||||
led_count: int = Field(description="Total number of LEDs")
|
led_count: int = Field(description="Total number of LEDs")
|
||||||
enabled: bool = Field(description="Whether device is enabled")
|
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(
|
auto_shutdown: bool = Field(
|
||||||
default=False, description="Restore device to idle state when targets stop"
|
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_id: str = Field(description="Device ID")
|
||||||
device_type: str = Field(default="wled", description="LED device type")
|
device_type: str = Field(default="wled", description="LED device type")
|
||||||
device_online: bool = Field(default=False, description="Whether device is reachable")
|
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_latency_ms: float | None = Field(None, description="Health check latency in ms")
|
||||||
device_name: Optional[str] = Field(None, description="Device name reported by firmware")
|
device_name: str | None = Field(None, description="Device name reported by firmware")
|
||||||
device_version: Optional[str] = Field(None, description="Firmware version")
|
device_version: str | None = Field(None, description="Firmware version")
|
||||||
device_led_count: Optional[int] = Field(None, description="LED count reported by device")
|
device_led_count: int | None = Field(None, description="LED count reported by device")
|
||||||
device_rgbw: Optional[bool] = Field(None, description="Whether device uses RGBW LEDs")
|
device_rgbw: bool | None = Field(None, description="Whether device uses RGBW LEDs")
|
||||||
device_led_type: Optional[str] = Field(
|
device_led_type: str | None = Field(
|
||||||
None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)"
|
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)"
|
None, description="Device-reported FPS (WLED internal refresh rate)"
|
||||||
)
|
)
|
||||||
device_last_checked: Optional[datetime] = Field(None, description="Last health check time")
|
device_last_checked: datetime | None = Field(None, description="Last health check time")
|
||||||
device_error: Optional[str] = Field(None, description="Last health check error")
|
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: bool = Field(default=False, description="Whether calibration test mode is active")
|
||||||
test_mode_edges: List[str] = Field(
|
test_mode_edges: List[str] = Field(
|
||||||
default_factory=list, description="Currently lit edges in test mode"
|
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")
|
device_type: str = Field(default="wled", description="Device type")
|
||||||
ip: str = Field(description="IP address")
|
ip: str = Field(description="IP address")
|
||||||
mac: str = Field(default="", description="MAC address")
|
mac: str = Field(default="", description="MAC address")
|
||||||
led_count: Optional[int] = Field(None, description="LED count (if reachable)")
|
led_count: int | None = Field(None, description="LED count (if reachable)")
|
||||||
version: Optional[str] = Field(None, description="Firmware version")
|
version: str | None = Field(None, description="Firmware version")
|
||||||
ble_family: Optional[str] = Field(
|
ble_family: str | None = Field(
|
||||||
None, description="Detected BLE protocol family (sp110e/triones/zengge/govee)"
|
None, description="Detected BLE protocol family (sp110e/triones/zengge/govee)"
|
||||||
)
|
)
|
||||||
already_added: bool = Field(
|
already_added: bool = Field(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Filter-related schemas."""
|
"""Filter-related schemas."""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -22,10 +22,10 @@ class FilterOptionDefSchema(BaseModel):
|
|||||||
min_value: Any = Field(description="Minimum value")
|
min_value: Any = Field(description="Minimum value")
|
||||||
max_value: Any = Field(description="Maximum value")
|
max_value: Any = Field(description="Maximum value")
|
||||||
step: Any = Field(description="Step increment")
|
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"
|
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"
|
default=None, description="Maximum string length for string type"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
"""Pydantic schemas for game integration API endpoints."""
|
"""Pydantic schemas for game integration API endpoints."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
# ── Event Mapping ──────────────────────────────────────────────────────────
|
# ── Event Mapping ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -40,14 +39,14 @@ class GameIntegrationCreate(BaseModel):
|
|||||||
event_mappings: List[EventMappingSchema] = Field(
|
event_mappings: List[EventMappingSchema] = Field(
|
||||||
default_factory=list, description="Event-to-effect mappings"
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
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):
|
class GameIntegrationUpdate(BaseModel):
|
||||||
"""Request to update a game integration config."""
|
"""Request to update a game integration config."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Integration name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Integration name", min_length=1, max_length=100)
|
||||||
adapter_type: Optional[str] = Field(None, description="Adapter type identifier", min_length=1)
|
adapter_type: str | None = Field(None, description="Adapter type identifier", min_length=1)
|
||||||
enabled: Optional[bool] = Field(None, description="Whether integration is active")
|
enabled: bool | None = Field(None, description="Whether integration is active")
|
||||||
adapter_config: Optional[Dict[str, Any]] = Field(None, description="Adapter-specific settings")
|
adapter_config: Dict[str, Any] | None = Field(None, description="Adapter-specific settings")
|
||||||
event_mappings: Optional[List[EventMappingSchema]] = Field(
|
event_mappings: List[EventMappingSchema] | None = Field(
|
||||||
None, description="Event-to-effect mappings"
|
None, 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: Optional[List[str]] = Field(None, description="User-defined tags")
|
tags: List[str] | None = Field(None, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
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")
|
event_mappings: List[EventMappingSchema] = Field(description="Event-to-effect mappings")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library.",
|
description="Icon id from the curated icon library.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon.",
|
description="Optional CSS color override for the icon.",
|
||||||
@@ -158,7 +157,7 @@ class GameIntegrationStatusResponse(BaseModel):
|
|||||||
integration_id: str = Field(description="Integration ID")
|
integration_id: str = Field(description="Integration ID")
|
||||||
enabled: bool = Field(description="Whether integration is active")
|
enabled: bool = Field(description="Whether integration is active")
|
||||||
connected: bool = Field(description="Whether adapter is currently receiving data")
|
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_count: int = Field(default=0, description="Total events received")
|
||||||
event_counts_by_type: Dict[str, int] = Field(
|
event_counts_by_type: Dict[str, int] = Field(
|
||||||
default_factory=dict, description="Event counts per event type"
|
default_factory=dict, description="Event counts per event type"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Gradient schemas (CRUD)."""
|
"""Gradient schemas (CRUD)."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -18,14 +18,14 @@ class GradientCreate(BaseModel):
|
|||||||
|
|
||||||
name: str = Field(description="Gradient name", min_length=1, max_length=100)
|
name: str = Field(description="Gradient name", min_length=1, max_length=100)
|
||||||
stops: List[GradientStopSchema] = Field(description="Color stops", min_length=2)
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -35,16 +35,16 @@ class GradientCreate(BaseModel):
|
|||||||
class GradientUpdate(BaseModel):
|
class GradientUpdate(BaseModel):
|
||||||
"""Request to update a gradient."""
|
"""Request to update a gradient."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Gradient name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Gradient name", min_length=1, max_length=100)
|
||||||
stops: Optional[List[GradientStopSchema]] = Field(None, description="Color stops", min_length=2)
|
stops: List[GradientStopSchema] | None = Field(None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
name: str = Field(description="Gradient name")
|
||||||
stops: List[GradientStopSchema] = Field(description="Color stops")
|
stops: List[GradientStopSchema] = Field(description="Color stops")
|
||||||
is_builtin: bool = Field(description="Whether this is a built-in gradient")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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)."""
|
"""Home Assistant source schemas (CRUD + test + entities)."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -16,14 +16,14 @@ class HomeAssistantSourceCreate(BaseModel):
|
|||||||
entity_filters: List[str] = Field(
|
entity_filters: List[str] = Field(
|
||||||
default_factory=list, description="Entity ID filter patterns (e.g. ['sensor.*'])"
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
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):
|
class HomeAssistantSourceUpdate(BaseModel):
|
||||||
"""Request to update a Home Assistant source."""
|
"""Request to update a Home Assistant source."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Source name", min_length=1, max_length=100)
|
||||||
host: Optional[str] = Field(None, description="HA host:port", min_length=1)
|
host: str | None = Field(None, description="HA host:port", min_length=1)
|
||||||
token: Optional[str] = Field(None, description="Long-Lived Access Token", min_length=1)
|
token: str | None = Field(None, description="Long-Lived Access Token", min_length=1)
|
||||||
use_ssl: Optional[bool] = Field(None, description="Use wss://")
|
use_ssl: bool | None = Field(None, description="Use wss://")
|
||||||
entity_filters: Optional[List[str]] = Field(None, description="Entity ID filter patterns")
|
entity_filters: List[str] | None = Field(None, description="Entity ID filter patterns")
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
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")
|
entity_filters: List[str] = Field(default_factory=list, description="Entity filter patterns")
|
||||||
connected: bool = Field(default=False, description="Whether the WebSocket connection is active")
|
connected: bool = Field(default=False, description="Whether the WebSocket connection is active")
|
||||||
entity_count: int = Field(default=0, description="Number of cached entities")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library.",
|
description="Icon id from the curated icon library.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon.",
|
description="Optional CSS color override for the icon.",
|
||||||
)
|
)
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
token: Optional[str] = Field(
|
token: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description=(
|
description=(
|
||||||
"Long-Lived Access Token. Redacted as '***' unless the request "
|
"Long-Lived Access Token. Redacted as '***' unless the request "
|
||||||
@@ -112,9 +112,9 @@ class HomeAssistantTestResponse(BaseModel):
|
|||||||
"""Connection test result."""
|
"""Connection test result."""
|
||||||
|
|
||||||
success: bool = Field(description="Whether connection and auth succeeded")
|
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")
|
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):
|
class HomeAssistantConnectionStatus(BaseModel):
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
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 urllib.parse import urlparse
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
# RFC 7230 token chars for header names + reject any control character in values.
|
# RFC 7230 token chars for header names + reject any control character in values.
|
||||||
_HEADER_NAME_RE = re.compile(r"^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$")
|
_HEADER_NAME_RE = re.compile(r"^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$")
|
||||||
_HEADER_CONTROL_CHARS_RE = re.compile(r"[\x00-\x1f\x7f]")
|
_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)
|
headers: Dict[str, str] = Field(default_factory=dict)
|
||||||
timeout_s: float = Field(default=10.0, gt=0)
|
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)
|
tags: List[str] = Field(default_factory=list)
|
||||||
icon: Optional[str] = Field(None, max_length=64)
|
icon: str | None = Field(None, max_length=64)
|
||||||
icon_color: Optional[str] = Field(None, max_length=32)
|
icon_color: str | None = Field(None, max_length=32)
|
||||||
|
|
||||||
@field_validator("headers")
|
@field_validator("headers")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -88,16 +87,16 @@ class HTTPEndpointUpdate(BaseModel):
|
|||||||
field (or send ``null``) to keep it.
|
field (or send ``null``) to keep it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
name: str | None = Field(None, min_length=1, max_length=100)
|
||||||
url: Optional[str] = Field(None, min_length=1)
|
url: str | None = Field(None, min_length=1)
|
||||||
method: Optional[Literal["GET", "HEAD"]] = None
|
method: Literal["GET", "HEAD"] | None = None
|
||||||
auth_token: Optional[str] = Field(None, description="null = keep existing; '' = clear.")
|
auth_token: str | None = Field(None, description="null = keep existing; '' = clear.")
|
||||||
headers: Optional[Dict[str, str]] = None
|
headers: Dict[str, str] | None = None
|
||||||
timeout_s: Optional[float] = Field(None, gt=0)
|
timeout_s: float | None = Field(None, gt=0)
|
||||||
description: Optional[str] = Field(None, max_length=500)
|
description: str | None = Field(None, max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(None, max_length=64)
|
icon: str | None = Field(None, max_length=64)
|
||||||
icon_color: Optional[str] = Field(None, max_length=32)
|
icon_color: str | None = Field(None, max_length=32)
|
||||||
|
|
||||||
@field_validator("headers")
|
@field_validator("headers")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -125,10 +124,10 @@ class HTTPEndpointResponse(BaseModel):
|
|||||||
auth_token_set: bool = False
|
auth_token_set: bool = False
|
||||||
headers: Dict[str, str] = Field(default_factory=dict)
|
headers: Dict[str, str] = Field(default_factory=dict)
|
||||||
timeout_s: float
|
timeout_s: float
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
tags: List[str] = Field(default_factory=list)
|
tags: List[str] = Field(default_factory=list)
|
||||||
icon: Optional[str] = Field(None, max_length=64)
|
icon: str | None = Field(None, max_length=64)
|
||||||
icon_color: Optional[str] = Field(None, max_length=32)
|
icon_color: str | None = Field(None, max_length=32)
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|
||||||
@@ -160,7 +159,7 @@ class HTTPTestRequest(BaseModel):
|
|||||||
|
|
||||||
class HTTPTestResponse(BaseModel):
|
class HTTPTestResponse(BaseModel):
|
||||||
success: bool
|
success: bool
|
||||||
status_code: Optional[int] = None
|
status_code: int | None = None
|
||||||
body_preview: Optional[str] = Field(None, description="First 500 chars of the body")
|
body_preview: str | None = Field(None, description="First 500 chars of the body")
|
||||||
body_json: Any = None
|
body_json: Any = None
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""MQTT source schemas (CRUD + test + status)."""
|
"""MQTT source schemas (CRUD + test + status)."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -16,14 +16,14 @@ class MQTTSourceCreate(BaseModel):
|
|||||||
password: str = Field(default="", description="Broker password (optional)")
|
password: str = Field(default="", description="Broker password (optional)")
|
||||||
client_id: str = Field(default="ledgrab", description="MQTT client ID")
|
client_id: str = Field(default="ledgrab", description="MQTT client ID")
|
||||||
base_topic: str = Field(default="ledgrab", description="Base topic prefix")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
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):
|
class MQTTSourceUpdate(BaseModel):
|
||||||
"""Request to update an MQTT source."""
|
"""Request to update an MQTT source."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
name: str | None = 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_host: str | None = 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)
|
broker_port: int | None = Field(None, description="MQTT broker port", ge=1, le=65535)
|
||||||
username: Optional[str] = Field(None, description="Broker username")
|
username: str | None = Field(None, description="Broker username")
|
||||||
password: Optional[str] = Field(None, description="Broker password")
|
password: str | None = Field(None, description="Broker password")
|
||||||
client_id: Optional[str] = Field(None, description="MQTT client ID")
|
client_id: str | None = Field(None, description="MQTT client ID")
|
||||||
base_topic: Optional[str] = Field(None, description="Base topic prefix")
|
base_topic: str | None = Field(None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
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")
|
client_id: str = Field(description="MQTT client ID")
|
||||||
base_topic: str = Field(description="Base topic prefix")
|
base_topic: str = Field(description="Base topic prefix")
|
||||||
connected: bool = Field(default=False, description="Whether the broker connection is active")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library.",
|
description="Icon id from the curated icon library.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon.",
|
description="Optional CSS color override for the icon.",
|
||||||
@@ -93,7 +93,7 @@ class MQTTTestResponse(BaseModel):
|
|||||||
"""Connection test result."""
|
"""Connection test result."""
|
||||||
|
|
||||||
success: bool = Field(description="Whether broker connection succeeded")
|
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):
|
class MQTTConnectionStatus(BaseModel):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Output target schemas — discriminated unions per target type."""
|
"""Output target schemas — discriminated unions per target type."""
|
||||||
|
|
||||||
from datetime import datetime
|
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
|
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
|
# 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}."""
|
"""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')")
|
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_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)")
|
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)"
|
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_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)")
|
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)"
|
default=1.0, description="Brightness multiplier (bindable)"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class _OutputTargetResponseBase(BaseModel):
|
|||||||
|
|
||||||
id: str = Field(description="Target ID")
|
id: str = Field(description="Target ID")
|
||||||
name: str = Field(description="Target name")
|
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")
|
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: 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")
|
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"
|
target_type: Literal["led"] = "led"
|
||||||
device_id: str = Field(default="", description="LED device ID")
|
device_id: str = Field(default="", description="LED device ID")
|
||||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||||
fps: Optional[BindableFloatInput] = Field(None, description="Target send FPS (bindable)")
|
fps: BindableFloatInput | None = Field(None, description="Target send FPS (bindable)")
|
||||||
keepalive_interval: float = Field(default=1.0, description="Keepalive interval (s)")
|
keepalive_interval: float = Field(default=1.0, description="Keepalive interval (s)")
|
||||||
state_check_interval: int = Field(
|
state_check_interval: int = Field(
|
||||||
default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)"
|
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)"
|
default=0, description="Min brightness threshold (bindable, 0=disabled)"
|
||||||
)
|
)
|
||||||
adaptive_fps: bool = Field(
|
adaptive_fps: bool = Field(
|
||||||
@@ -110,20 +110,20 @@ class HALightOutputTargetResponse(_OutputTargetResponseBase):
|
|||||||
description="Colour value source ID (used when source_kind='color_vs'); "
|
description="Colour value source ID (used when source_kind='color_vs'); "
|
||||||
"must reference a value source whose return_type='color'.",
|
"must reference a value source whose return_type='color'.",
|
||||||
)
|
)
|
||||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
ha_light_mappings: List[HALightMappingSchema] | None = Field(
|
||||||
None, description="LED-to-light mappings"
|
None, description="LED-to-light mappings"
|
||||||
)
|
)
|
||||||
update_rate: Optional[BindableFloatInput] = Field(
|
update_rate: BindableFloatInput | None = Field(
|
||||||
None, description="Service call rate Hz (bindable)"
|
None, description="Service call rate Hz (bindable)"
|
||||||
)
|
)
|
||||||
transition: Optional[BindableFloatInput] = Field(
|
transition: BindableFloatInput | None = Field(
|
||||||
None, description="HA transition seconds (bindable)"
|
None, description="HA transition seconds (bindable)"
|
||||||
)
|
)
|
||||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
color_tolerance: BindableFloatInput | None = Field(
|
||||||
None, description="RGB delta tolerance (bindable)"
|
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)"
|
default=0, description="Min brightness threshold (bindable, 0=disabled)"
|
||||||
)
|
)
|
||||||
stop_action: Literal["none", "turn_off", "restore"] = Field(
|
stop_action: Literal["none", "turn_off", "restore"] = Field(
|
||||||
@@ -151,24 +151,24 @@ class Z2MLightOutputTargetResponse(_OutputTargetResponseBase):
|
|||||||
default="",
|
default="",
|
||||||
description="Colour value source ID (used when source_kind='color_vs').",
|
description="Colour value source ID (used when source_kind='color_vs').",
|
||||||
)
|
)
|
||||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||||
z2m_light_mappings: Optional[List[Z2MLightMappingSchema]] = Field(
|
z2m_light_mappings: List[Z2MLightMappingSchema] | None = Field(
|
||||||
None, description="LED-to-bulb mappings (by Z2M friendly_name)"
|
None, description="LED-to-bulb mappings (by Z2M friendly_name)"
|
||||||
)
|
)
|
||||||
base_topic: str = Field(
|
base_topic: str = Field(
|
||||||
default="zigbee2mqtt",
|
default="zigbee2mqtt",
|
||||||
description="Z2M MQTT base topic prefix (override if your Z2M instance is non-default).",
|
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)"
|
None, description="Publish rate Hz (bindable; 0.5-10)"
|
||||||
)
|
)
|
||||||
transition: Optional[BindableFloatInput] = Field(
|
transition: BindableFloatInput | None = Field(
|
||||||
None, description="Z2M transition seconds (bindable)"
|
None, description="Z2M transition seconds (bindable)"
|
||||||
)
|
)
|
||||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
color_tolerance: BindableFloatInput | None = Field(
|
||||||
None, description="RGB delta tolerance (bindable)"
|
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)"
|
default=0, description="Min brightness threshold (bindable, 0=disabled)"
|
||||||
)
|
)
|
||||||
stop_action: Literal["none", "turn_off"] = Field(
|
stop_action: Literal["none", "turn_off"] = Field(
|
||||||
@@ -179,11 +179,9 @@ class Z2MLightOutputTargetResponse(_OutputTargetResponseBase):
|
|||||||
|
|
||||||
|
|
||||||
OutputTargetResponse = Annotated[
|
OutputTargetResponse = Annotated[
|
||||||
Union[
|
Annotated[LedOutputTargetResponse, Tag("led")]
|
||||||
Annotated[LedOutputTargetResponse, Tag("led")],
|
| Annotated[HALightOutputTargetResponse, Tag("ha_light")]
|
||||||
Annotated[HALightOutputTargetResponse, Tag("ha_light")],
|
| Annotated[Z2MLightOutputTargetResponse, Tag("z2m_light")],
|
||||||
Annotated[Z2MLightOutputTargetResponse, Tag("z2m_light")],
|
|
||||||
],
|
|
||||||
Discriminator("target_type"),
|
Discriminator("target_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -196,12 +194,12 @@ class _OutputTargetCreateBase(BaseModel):
|
|||||||
"""Shared fields for all output target create requests."""
|
"""Shared fields for all output target create requests."""
|
||||||
|
|
||||||
name: str = Field(description="Target name", min_length=1, max_length=100)
|
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")
|
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"
|
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"
|
None, max_length=32, description="Optional CSS color override for the icon"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -210,10 +208,8 @@ class LedOutputTargetCreate(_OutputTargetCreateBase):
|
|||||||
target_type: Literal["led"] = "led"
|
target_type: Literal["led"] = "led"
|
||||||
device_id: str = Field(default="", description="LED device ID")
|
device_id: str = Field(default="", description="LED device ID")
|
||||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||||
brightness: Optional[BindableFloatInput] = Field(
|
brightness: BindableFloatInput | None = Field(default=1.0, description="Brightness (bindable)")
|
||||||
default=1.0, description="Brightness (bindable)"
|
fps: BindableFloatInput | None = Field(
|
||||||
)
|
|
||||||
fps: Optional[BindableFloatInput] = Field(
|
|
||||||
default=30, description="Target send FPS (bindable, 1-90)"
|
default=30, description="Target send FPS (bindable, 1-90)"
|
||||||
)
|
)
|
||||||
keepalive_interval: float = Field(
|
keepalive_interval: float = Field(
|
||||||
@@ -228,7 +224,7 @@ class LedOutputTargetCreate(_OutputTargetCreateBase):
|
|||||||
ge=5,
|
ge=5,
|
||||||
le=600,
|
le=600,
|
||||||
)
|
)
|
||||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||||
default=0,
|
default=0,
|
||||||
description="Min brightness threshold (bindable, 0=disabled); below this -> off",
|
description="Min brightness threshold (bindable, 0=disabled); below this -> off",
|
||||||
)
|
)
|
||||||
@@ -257,22 +253,20 @@ class HALightOutputTargetCreate(_OutputTargetCreateBase):
|
|||||||
default="",
|
default="",
|
||||||
description="Colour value source ID (used when source_kind='color_vs').",
|
description="Colour value source ID (used when source_kind='color_vs').",
|
||||||
)
|
)
|
||||||
brightness: Optional[BindableFloatInput] = Field(
|
brightness: BindableFloatInput | None = Field(default=1.0, description="Brightness (bindable)")
|
||||||
default=1.0, description="Brightness (bindable)"
|
ha_light_mappings: List[HALightMappingSchema] | None = Field(
|
||||||
)
|
|
||||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
|
||||||
None, description="LED-to-light mappings"
|
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)"
|
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)"
|
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)"
|
default=5, description="RGB delta tolerance (bindable)"
|
||||||
)
|
)
|
||||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||||
default=0,
|
default=0,
|
||||||
description="Min brightness threshold (bindable, 0=disabled); below this -> off",
|
description="Min brightness threshold (bindable, 0=disabled); below this -> off",
|
||||||
)
|
)
|
||||||
@@ -299,10 +293,8 @@ class Z2MLightOutputTargetCreate(_OutputTargetCreateBase):
|
|||||||
default="",
|
default="",
|
||||||
description="Colour value source ID (used when source_kind='color_vs').",
|
description="Colour value source ID (used when source_kind='color_vs').",
|
||||||
)
|
)
|
||||||
brightness: Optional[BindableFloatInput] = Field(
|
brightness: BindableFloatInput | None = Field(default=1.0, description="Brightness (bindable)")
|
||||||
default=1.0, description="Brightness (bindable)"
|
z2m_light_mappings: List[Z2MLightMappingSchema] | None = Field(
|
||||||
)
|
|
||||||
z2m_light_mappings: Optional[List[Z2MLightMappingSchema]] = Field(
|
|
||||||
None, description="LED-to-bulb mappings (by Z2M friendly_name)"
|
None, description="LED-to-bulb mappings (by Z2M friendly_name)"
|
||||||
)
|
)
|
||||||
base_topic: str = Field(
|
base_topic: str = Field(
|
||||||
@@ -310,16 +302,16 @@ class Z2MLightOutputTargetCreate(_OutputTargetCreateBase):
|
|||||||
max_length=128,
|
max_length=128,
|
||||||
description="Z2M MQTT base topic prefix.",
|
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)"
|
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)"
|
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)"
|
default=5, description="RGB delta tolerance (bindable)"
|
||||||
)
|
)
|
||||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||||
default=0,
|
default=0,
|
||||||
description="Min brightness threshold (bindable, 0=disabled); below this -> off",
|
description="Min brightness threshold (bindable, 0=disabled); below this -> off",
|
||||||
)
|
)
|
||||||
@@ -330,11 +322,9 @@ class Z2MLightOutputTargetCreate(_OutputTargetCreateBase):
|
|||||||
|
|
||||||
|
|
||||||
OutputTargetCreate = Annotated[
|
OutputTargetCreate = Annotated[
|
||||||
Union[
|
Annotated[LedOutputTargetCreate, Tag("led")]
|
||||||
Annotated[LedOutputTargetCreate, Tag("led")],
|
| Annotated[HALightOutputTargetCreate, Tag("ha_light")]
|
||||||
Annotated[HALightOutputTargetCreate, Tag("ha_light")],
|
| Annotated[Z2MLightOutputTargetCreate, Tag("z2m_light")],
|
||||||
Annotated[Z2MLightOutputTargetCreate, Tag("z2m_light")],
|
|
||||||
],
|
|
||||||
Discriminator("target_type"),
|
Discriminator("target_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -346,15 +336,15 @@ OutputTargetCreate = Annotated[
|
|||||||
class _OutputTargetUpdateBase(BaseModel):
|
class _OutputTargetUpdateBase(BaseModel):
|
||||||
"""Shared fields for all output target update requests."""
|
"""Shared fields for all output target update requests."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Target name", min_length=1, max_length=100)
|
name: str | None = Field(None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Custom icon id; pass empty string to clear and inherit from device.",
|
description="Custom icon id; pass empty string to clear and inherit from device.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon; empty string clears.",
|
description="Optional CSS color override for the icon; empty string clears.",
|
||||||
@@ -363,103 +353,99 @@ class _OutputTargetUpdateBase(BaseModel):
|
|||||||
|
|
||||||
class LedOutputTargetUpdate(_OutputTargetUpdateBase):
|
class LedOutputTargetUpdate(_OutputTargetUpdateBase):
|
||||||
target_type: Literal["led"] = "led"
|
target_type: Literal["led"] = "led"
|
||||||
device_id: Optional[str] = Field(None, description="LED device ID")
|
device_id: str | None = Field(None, description="LED device ID")
|
||||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
color_strip_source_id: str | None = Field(None, description="Color strip source ID")
|
||||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||||
fps: Optional[BindableFloatInput] = Field(None, description="Target send FPS (bindable, 1-90)")
|
fps: BindableFloatInput | None = Field(None, description="Target send FPS (bindable, 1-90)")
|
||||||
keepalive_interval: Optional[float] = Field(
|
keepalive_interval: float | None = Field(
|
||||||
None, description="Keepalive interval (0.5-5.0s)", ge=0.5, le=5.0
|
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
|
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)"
|
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"
|
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)"
|
None, pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HALightOutputTargetUpdate(_OutputTargetUpdateBase):
|
class HALightOutputTargetUpdate(_OutputTargetUpdateBase):
|
||||||
target_type: Literal["ha_light"] = "ha_light"
|
target_type: Literal["ha_light"] = "ha_light"
|
||||||
ha_source_id: Optional[str] = Field(None, description="Home Assistant source ID")
|
ha_source_id: str | None = Field(None, description="Home Assistant source ID")
|
||||||
source_kind: Optional[Literal["css", "color_vs"]] = Field(
|
source_kind: Literal["css", "color_vs"] | None = Field(
|
||||||
None,
|
None,
|
||||||
description="Colour source kind: 'css' or 'color_vs'.",
|
description="Colour source kind: 'css' or 'color_vs'.",
|
||||||
)
|
)
|
||||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
color_strip_source_id: str | None = Field(None, description="Color strip source ID")
|
||||||
color_value_source_id: Optional[str] = Field(
|
color_value_source_id: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description="Colour value source ID (used when source_kind='color_vs').",
|
description="Colour value source ID (used when source_kind='color_vs').",
|
||||||
)
|
)
|
||||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
ha_light_mappings: List[HALightMappingSchema] | None = Field(
|
||||||
None, description="LED-to-light mappings"
|
None, description="LED-to-light mappings"
|
||||||
)
|
)
|
||||||
update_rate: Optional[BindableFloatInput] = Field(
|
update_rate: BindableFloatInput | None = Field(
|
||||||
None, description="Service call rate Hz (bindable)"
|
None, description="Service call rate Hz (bindable)"
|
||||||
)
|
)
|
||||||
transition: Optional[BindableFloatInput] = Field(
|
transition: BindableFloatInput | None = Field(
|
||||||
None, description="HA transition seconds (bindable)"
|
None, description="HA transition seconds (bindable)"
|
||||||
)
|
)
|
||||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
color_tolerance: BindableFloatInput | None = Field(
|
||||||
None, description="RGB delta tolerance (bindable)"
|
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)"
|
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'."
|
None, description="Finalization on stop: 'none', 'turn_off', or 'restore'."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Z2MLightOutputTargetUpdate(_OutputTargetUpdateBase):
|
class Z2MLightOutputTargetUpdate(_OutputTargetUpdateBase):
|
||||||
target_type: Literal["z2m_light"] = "z2m_light"
|
target_type: Literal["z2m_light"] = "z2m_light"
|
||||||
mqtt_source_id: Optional[str] = Field(
|
mqtt_source_id: str | None = Field(
|
||||||
None,
|
None,
|
||||||
description="MQTT source (broker) id. Empty string clears the binding.",
|
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'."
|
None, description="Colour source kind: 'css' or 'color_vs'."
|
||||||
)
|
)
|
||||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
color_strip_source_id: str | None = Field(None, description="Color strip source ID")
|
||||||
color_value_source_id: Optional[str] = Field(
|
color_value_source_id: str | None = Field(
|
||||||
None, description="Colour value source ID (used when source_kind='color_vs')."
|
None, description="Colour value source ID (used when source_kind='color_vs')."
|
||||||
)
|
)
|
||||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||||
z2m_light_mappings: Optional[List[Z2MLightMappingSchema]] = Field(
|
z2m_light_mappings: List[Z2MLightMappingSchema] | None = Field(
|
||||||
None, description="LED-to-bulb mappings (by Z2M friendly_name)"
|
None, description="LED-to-bulb mappings (by Z2M friendly_name)"
|
||||||
)
|
)
|
||||||
base_topic: Optional[str] = Field(
|
base_topic: str | None = Field(None, max_length=128, description="Z2M MQTT base topic prefix.")
|
||||||
None, max_length=128, description="Z2M MQTT base topic prefix."
|
update_rate: BindableFloatInput | None = Field(
|
||||||
)
|
|
||||||
update_rate: Optional[BindableFloatInput] = Field(
|
|
||||||
None, description="Publish rate Hz (bindable; 0.5-10)"
|
None, description="Publish rate Hz (bindable; 0.5-10)"
|
||||||
)
|
)
|
||||||
transition: Optional[BindableFloatInput] = Field(
|
transition: BindableFloatInput | None = Field(
|
||||||
None, description="Z2M transition seconds (bindable)"
|
None, description="Z2M transition seconds (bindable)"
|
||||||
)
|
)
|
||||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
color_tolerance: BindableFloatInput | None = Field(
|
||||||
None, description="RGB delta tolerance (bindable)"
|
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)"
|
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'."
|
None, description="Finalization on stop: 'none' or 'turn_off'."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
OutputTargetUpdate = Annotated[
|
OutputTargetUpdate = Annotated[
|
||||||
Union[
|
Annotated[LedOutputTargetUpdate, Tag("led")]
|
||||||
Annotated[LedOutputTargetUpdate, Tag("led")],
|
| Annotated[HALightOutputTargetUpdate, Tag("ha_light")]
|
||||||
Annotated[HALightOutputTargetUpdate, Tag("ha_light")],
|
| Annotated[Z2MLightOutputTargetUpdate, Tag("z2m_light")],
|
||||||
Annotated[Z2MLightOutputTargetUpdate, Tag("z2m_light")],
|
|
||||||
],
|
|
||||||
Discriminator("target_type"),
|
Discriminator("target_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -479,75 +465,69 @@ class TargetProcessingState(BaseModel):
|
|||||||
"""Processing state for an output target."""
|
"""Processing state for an output target."""
|
||||||
|
|
||||||
target_id: str = Field(description="Target ID")
|
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")
|
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||||
processing: bool = Field(description="Whether processing is active")
|
processing: bool = Field(description="Whether processing is active")
|
||||||
fps_actual: Optional[float] = Field(None, description="Actual FPS achieved")
|
fps_actual: float | None = Field(None, description="Actual FPS achieved")
|
||||||
fps_potential: Optional[float] = Field(
|
fps_potential: float | None = Field(
|
||||||
None, description="Potential FPS (processing speed without throttle)"
|
None, description="Potential FPS (processing speed without throttle)"
|
||||||
)
|
)
|
||||||
fps_target: Optional[int] = Field(None, description="Target FPS")
|
fps_target: int | None = Field(None, description="Target FPS")
|
||||||
fps_capture: Optional[int] = Field(
|
fps_capture: int | None = Field(
|
||||||
None, description="Configured capture-side FPS for the underlying color strip stream"
|
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_skipped: int | None = Field(None, description="Frames skipped (no screen change)")
|
||||||
frames_keepalive: Optional[int] = Field(
|
frames_keepalive: int | None = Field(None, description="Keepalive frames sent during standby")
|
||||||
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)")
|
||||||
fps_current: Optional[int] = Field(None, description="Frames sent in the last second")
|
timing_extract_ms: float | None = Field(None, description="Border pixel extraction time (ms)")
|
||||||
timing_send_ms: Optional[float] = Field(None, description="DDP send time (ms)")
|
timing_map_leds_ms: float | None = Field(None, description="LED color mapping time (ms)")
|
||||||
timing_extract_ms: Optional[float] = Field(
|
timing_smooth_ms: float | None = Field(None, description="Temporal smoothing time (ms)")
|
||||||
None, description="Border pixel extraction 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_map_leds_ms: Optional[float] = Field(None, description="LED color mapping time (ms)")
|
timing_audio_fft_ms: float | None = Field(None, description="Audio FFT analysis time (ms)")
|
||||||
timing_smooth_ms: Optional[float] = Field(None, description="Temporal smoothing time (ms)")
|
timing_audio_render_ms: float | None = Field(
|
||||||
timing_total_ms: Optional[float] = Field(
|
|
||||||
None, description="Total processing time per frame (ms)"
|
|
||||||
)
|
|
||||||
timing_audio_read_ms: Optional[float] = Field(None, description="Audio device read time (ms)")
|
|
||||||
timing_audio_fft_ms: Optional[float] = Field(None, description="Audio FFT analysis time (ms)")
|
|
||||||
timing_audio_render_ms: Optional[float] = Field(
|
|
||||||
None, description="Audio visualization render time (ms)"
|
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(
|
overlay_active: bool = Field(
|
||||||
default=False, description="Whether visualization overlay is active"
|
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")
|
errors: List[str] = Field(default_factory=list, description="Recent errors")
|
||||||
device_online: bool = Field(default=False, description="Whether device is reachable")
|
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_latency_ms: float | None = Field(None, description="Health check latency in ms")
|
||||||
device_name: Optional[str] = Field(None, description="Device name reported by firmware")
|
device_name: str | None = Field(None, description="Device name reported by firmware")
|
||||||
device_version: Optional[str] = Field(None, description="Firmware version")
|
device_version: str | None = Field(None, description="Firmware version")
|
||||||
device_led_count: Optional[int] = Field(None, description="LED count reported by device")
|
device_led_count: int | None = Field(None, description="LED count reported by device")
|
||||||
device_rgbw: Optional[bool] = Field(None, description="Whether device uses RGBW LEDs")
|
device_rgbw: bool | None = Field(None, description="Whether device uses RGBW LEDs")
|
||||||
device_led_type: Optional[str] = Field(
|
device_led_type: str | None = Field(
|
||||||
None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)"
|
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)"
|
None, description="Device-reported FPS (WLED internal refresh rate)"
|
||||||
)
|
)
|
||||||
device_last_checked: Optional[datetime] = Field(None, description="Last health check time")
|
device_last_checked: datetime | None = Field(None, description="Last health check time")
|
||||||
device_error: Optional[str] = Field(None, description="Last health check error")
|
device_error: str | None = Field(None, description="Last health check error")
|
||||||
device_streaming_reachable: Optional[bool] = Field(
|
device_streaming_reachable: bool | None = Field(
|
||||||
None, description="Device reachable during streaming (HTTP probe)"
|
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):
|
class TargetMetricsResponse(BaseModel):
|
||||||
"""Target metrics response."""
|
"""Target metrics response."""
|
||||||
|
|
||||||
target_id: str = Field(description="Target ID")
|
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")
|
processing: bool = Field(description="Whether processing is active")
|
||||||
fps_actual: Optional[float] = Field(None, description="Actual FPS")
|
fps_actual: float | None = Field(None, description="Actual FPS")
|
||||||
fps_target: Optional[int] = Field(None, description="Target FPS")
|
fps_target: int | None = Field(None, description="Target FPS")
|
||||||
uptime_seconds: float = Field(description="Processing uptime in seconds")
|
uptime_seconds: float = Field(description="Processing uptime in seconds")
|
||||||
frames_processed: int = Field(description="Total frames processed")
|
frames_processed: int = Field(description="Total frames processed")
|
||||||
errors_count: int = Field(description="Total error count")
|
errors_count: int = Field(description="Total error count")
|
||||||
last_error: Optional[str] = Field(None, description="Last error message")
|
last_error: str | None = Field(None, description="Last error message")
|
||||||
last_update: Optional[datetime] = Field(None, description="Last update timestamp")
|
last_update: datetime | None = Field(None, description="Last update timestamp")
|
||||||
|
|
||||||
|
|
||||||
class BulkTargetRequest(BaseModel):
|
class BulkTargetRequest(BaseModel):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Pydantic schemas for pattern template API."""
|
"""Pydantic schemas for pattern template API."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -15,14 +15,14 @@ class PatternTemplateCreate(BaseModel):
|
|||||||
rectangles: List[KeyColorRectangleSchema] = Field(
|
rectangles: List[KeyColorRectangleSchema] = Field(
|
||||||
default_factory=list, description="List of named rectangles"
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -32,18 +32,18 @@ class PatternTemplateCreate(BaseModel):
|
|||||||
class PatternTemplateUpdate(BaseModel):
|
class PatternTemplateUpdate(BaseModel):
|
||||||
"""Request to update a pattern template."""
|
"""Request to update a pattern template."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||||
rectangles: Optional[List[KeyColorRectangleSchema]] = Field(
|
rectangles: List[KeyColorRectangleSchema] | None = Field(
|
||||||
None, description="List of named rectangles"
|
None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
description: str | None = Field(None, description="Template description")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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."""
|
"""Picture source schemas — discriminated unions per stream type."""
|
||||||
|
|
||||||
from datetime import datetime
|
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
|
from pydantic import BaseModel, Discriminator, Field, Tag
|
||||||
|
|
||||||
@@ -15,16 +15,16 @@ class _PictureSourceResponseBase(BaseModel):
|
|||||||
|
|
||||||
id: str = Field(description="Stream ID")
|
id: str = Field(description="Stream ID")
|
||||||
name: str = Field(description="Stream name")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -46,28 +46,26 @@ class ProcessedPictureSourceResponse(_PictureSourceResponseBase):
|
|||||||
|
|
||||||
class StaticImagePictureSourceResponse(_PictureSourceResponseBase):
|
class StaticImagePictureSourceResponse(_PictureSourceResponseBase):
|
||||||
stream_type: Literal["static_image"] = "static_image"
|
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):
|
class VideoPictureSourceResponse(_PictureSourceResponseBase):
|
||||||
stream_type: Literal["video"] = "video"
|
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")
|
loop: bool = Field(True, description="Loop video playback")
|
||||||
playback_speed: float = Field(1.0, description="Playback speed multiplier")
|
playback_speed: float = Field(1.0, description="Playback speed multiplier")
|
||||||
start_time: Optional[float] = Field(None, description="Trim start time in seconds")
|
start_time: float | None = Field(None, description="Trim start time in seconds")
|
||||||
end_time: Optional[float] = Field(None, description="Trim end time in seconds")
|
end_time: float | None = Field(None, description="Trim end time in seconds")
|
||||||
resolution_limit: Optional[int] = Field(None, description="Max width for decode")
|
resolution_limit: int | None = Field(None, description="Max width for decode")
|
||||||
clock_id: Optional[str] = Field(None, description="Sync clock ID")
|
clock_id: str | None = Field(None, description="Sync clock ID")
|
||||||
target_fps: int = Field(30, description="Target FPS")
|
target_fps: int = Field(30, description="Target FPS")
|
||||||
|
|
||||||
|
|
||||||
PictureSourceResponse = Annotated[
|
PictureSourceResponse = Annotated[
|
||||||
Union[
|
Annotated[RawPictureSourceResponse, Tag("raw")]
|
||||||
Annotated[RawPictureSourceResponse, Tag("raw")],
|
| Annotated[ProcessedPictureSourceResponse, Tag("processed")]
|
||||||
Annotated[ProcessedPictureSourceResponse, Tag("processed")],
|
| Annotated[StaticImagePictureSourceResponse, Tag("static_image")]
|
||||||
Annotated[StaticImagePictureSourceResponse, Tag("static_image")],
|
| Annotated[VideoPictureSourceResponse, Tag("video")],
|
||||||
Annotated[VideoPictureSourceResponse, Tag("video")],
|
|
||||||
],
|
|
||||||
Discriminator("stream_type"),
|
Discriminator("stream_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -80,14 +78,14 @@ class _PictureSourceCreateBase(BaseModel):
|
|||||||
"""Shared fields for all picture source create requests."""
|
"""Shared fields for all picture source create requests."""
|
||||||
|
|
||||||
name: str = Field(description="Stream name", min_length=1, max_length=100)
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
video_asset_id: str = Field(description="Video asset ID")
|
||||||
loop: bool = Field(True, description="Loop video playback")
|
loop: bool = Field(True, description="Loop video playback")
|
||||||
playback_speed: float = Field(1.0, description="Playback speed multiplier", ge=0.1, le=10.0)
|
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)
|
start_time: float | None = Field(None, description="Trim start time in seconds", ge=0)
|
||||||
end_time: Optional[float] = Field(None, description="Trim end time in seconds", ge=0)
|
end_time: float | None = Field(None, description="Trim end time in seconds", ge=0)
|
||||||
resolution_limit: Optional[int] = Field(
|
resolution_limit: int | None = Field(
|
||||||
None, description="Max width in pixels for decode downscale", ge=64, le=7680
|
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)
|
target_fps: int = Field(30, description="Target FPS", ge=1, le=90)
|
||||||
|
|
||||||
|
|
||||||
PictureSourceCreate = Annotated[
|
PictureSourceCreate = Annotated[
|
||||||
Union[
|
Annotated[RawPictureSourceCreate, Tag("raw")]
|
||||||
Annotated[RawPictureSourceCreate, Tag("raw")],
|
| Annotated[ProcessedPictureSourceCreate, Tag("processed")]
|
||||||
Annotated[ProcessedPictureSourceCreate, Tag("processed")],
|
| Annotated[StaticImagePictureSourceCreate, Tag("static_image")]
|
||||||
Annotated[StaticImagePictureSourceCreate, Tag("static_image")],
|
| Annotated[VideoPictureSourceCreate, Tag("video")],
|
||||||
Annotated[VideoPictureSourceCreate, Tag("video")],
|
|
||||||
],
|
|
||||||
Discriminator("stream_type"),
|
Discriminator("stream_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -144,15 +140,15 @@ PictureSourceCreate = Annotated[
|
|||||||
class _PictureSourceUpdateBase(BaseModel):
|
class _PictureSourceUpdateBase(BaseModel):
|
||||||
"""Shared fields for all picture source update requests."""
|
"""Shared fields for all picture source update requests."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Stream name", min_length=1, max_length=100)
|
name: str | None = Field(None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -161,47 +157,43 @@ class _PictureSourceUpdateBase(BaseModel):
|
|||||||
|
|
||||||
class RawPictureSourceUpdate(_PictureSourceUpdateBase):
|
class RawPictureSourceUpdate(_PictureSourceUpdateBase):
|
||||||
stream_type: Literal["raw"] = "raw"
|
stream_type: Literal["raw"] = "raw"
|
||||||
display_index: Optional[int] = Field(None, description="Display index", ge=0)
|
display_index: int | None = Field(None, description="Display index", ge=0)
|
||||||
capture_template_id: Optional[str] = Field(None, description="Capture template ID")
|
capture_template_id: str | None = Field(None, description="Capture template ID")
|
||||||
target_fps: Optional[int] = Field(None, description="Target FPS", ge=1, le=90)
|
target_fps: int | None = Field(None, description="Target FPS", ge=1, le=90)
|
||||||
|
|
||||||
|
|
||||||
class ProcessedPictureSourceUpdate(_PictureSourceUpdateBase):
|
class ProcessedPictureSourceUpdate(_PictureSourceUpdateBase):
|
||||||
stream_type: Literal["processed"] = "processed"
|
stream_type: Literal["processed"] = "processed"
|
||||||
source_stream_id: Optional[str] = Field(None, description="Source stream ID")
|
source_stream_id: str | None = Field(None, description="Source stream ID")
|
||||||
postprocessing_template_id: Optional[str] = Field(
|
postprocessing_template_id: str | None = Field(None, description="Postprocessing template ID")
|
||||||
None, description="Postprocessing template ID"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StaticImagePictureSourceUpdate(_PictureSourceUpdateBase):
|
class StaticImagePictureSourceUpdate(_PictureSourceUpdateBase):
|
||||||
stream_type: Literal["static_image"] = "static_image"
|
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):
|
class VideoPictureSourceUpdate(_PictureSourceUpdateBase):
|
||||||
stream_type: Literal["video"] = "video"
|
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: Optional[bool] = Field(None, description="Loop video playback")
|
loop: bool | None = Field(None, description="Loop video playback")
|
||||||
playback_speed: Optional[float] = Field(
|
playback_speed: float | None = Field(
|
||||||
None, description="Playback speed multiplier", ge=0.1, le=10.0
|
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)
|
start_time: float | None = Field(None, description="Trim start time in seconds", ge=0)
|
||||||
end_time: Optional[float] = Field(None, description="Trim end time in seconds", ge=0)
|
end_time: float | None = Field(None, description="Trim end time in seconds", ge=0)
|
||||||
resolution_limit: Optional[int] = Field(
|
resolution_limit: int | None = Field(
|
||||||
None, description="Max width in pixels for decode downscale", ge=64, le=7680
|
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: Optional[int] = Field(None, description="Target FPS", ge=1, le=90)
|
target_fps: int | None = Field(None, description="Target FPS", ge=1, le=90)
|
||||||
|
|
||||||
|
|
||||||
PictureSourceUpdate = Annotated[
|
PictureSourceUpdate = Annotated[
|
||||||
Union[
|
Annotated[RawPictureSourceUpdate, Tag("raw")]
|
||||||
Annotated[RawPictureSourceUpdate, Tag("raw")],
|
| Annotated[ProcessedPictureSourceUpdate, Tag("processed")]
|
||||||
Annotated[ProcessedPictureSourceUpdate, Tag("processed")],
|
| Annotated[StaticImagePictureSourceUpdate, Tag("static_image")]
|
||||||
Annotated[StaticImagePictureSourceUpdate, Tag("static_image")],
|
| Annotated[VideoPictureSourceUpdate, Tag("video")],
|
||||||
Annotated[VideoPictureSourceUpdate, Tag("video")],
|
|
||||||
],
|
|
||||||
Discriminator("stream_type"),
|
Discriminator("stream_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -246,7 +238,7 @@ class ImageValidateResponse(BaseModel):
|
|||||||
"""Response from image validation."""
|
"""Response from image validation."""
|
||||||
|
|
||||||
valid: bool = Field(description="Whether the image source is accessible and valid")
|
valid: bool = Field(description="Whether the image source is accessible and valid")
|
||||||
width: Optional[int] = Field(None, description="Image width in pixels")
|
width: int | None = Field(None, description="Image width in pixels")
|
||||||
height: Optional[int] = Field(None, description="Image height in pixels")
|
height: int | None = Field(None, description="Image height in pixels")
|
||||||
preview: Optional[str] = Field(None, description="Base64-encoded JPEG thumbnail")
|
preview: str | None = Field(None, description="Base64-encoded JPEG thumbnail")
|
||||||
error: Optional[str] = Field(None, description="Error message if invalid")
|
error: str | None = Field(None, description="Error message if invalid")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Postprocessing template schemas."""
|
"""Postprocessing template schemas."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -15,14 +15,14 @@ class PostprocessingTemplateCreate(BaseModel):
|
|||||||
filters: List[FilterInstanceSchema] = Field(
|
filters: List[FilterInstanceSchema] = Field(
|
||||||
default_factory=list, description="Ordered list of filter instances"
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -32,18 +32,18 @@ class PostprocessingTemplateCreate(BaseModel):
|
|||||||
class PostprocessingTemplateUpdate(BaseModel):
|
class PostprocessingTemplateUpdate(BaseModel):
|
||||||
"""Request to update a postprocessing template."""
|
"""Request to update a postprocessing template."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||||
filters: Optional[List[FilterInstanceSchema]] = Field(
|
filters: List[FilterInstanceSchema] | None = Field(
|
||||||
None, description="Ordered list of filter instances"
|
None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
description: str | None = Field(None, description="Template description")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Scene preset API schemas."""
|
"""Scene preset API schemas."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -19,16 +19,14 @@ class ScenePresetCreate(BaseModel):
|
|||||||
|
|
||||||
name: str = Field(description="Preset name", min_length=1, max_length=100)
|
name: str = Field(description="Preset name", min_length=1, max_length=100)
|
||||||
description: str = Field(default="", max_length=500)
|
description: str = Field(default="", max_length=500)
|
||||||
target_ids: Optional[List[str]] = Field(
|
target_ids: List[str] | None = Field(None, description="Target IDs to capture (all if omitted)")
|
||||||
None, description="Target IDs to capture (all if omitted)"
|
|
||||||
)
|
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -38,20 +36,20 @@ class ScenePresetCreate(BaseModel):
|
|||||||
class ScenePresetUpdate(BaseModel):
|
class ScenePresetUpdate(BaseModel):
|
||||||
"""Update scene preset metadata and optionally change which targets are included."""
|
"""Update scene preset metadata and optionally change which targets are included."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
name: str | None = Field(None, min_length=1, max_length=100)
|
||||||
description: Optional[str] = Field(None, max_length=500)
|
description: str | None = Field(None, max_length=500)
|
||||||
order: Optional[int] = None
|
order: int | None = None
|
||||||
target_ids: Optional[List[str]] = Field(
|
target_ids: List[str] | None = Field(
|
||||||
None,
|
None,
|
||||||
description="Update target list: keep state for existing, capture fresh for new, drop removed",
|
description="Update target list: keep state for existing, capture fresh for new, drop removed",
|
||||||
)
|
)
|
||||||
tags: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -67,12 +65,12 @@ class ScenePresetResponse(BaseModel):
|
|||||||
targets: List[TargetSnapshotSchema]
|
targets: List[TargetSnapshotSchema]
|
||||||
order: int
|
order: int
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Sync clock schemas (CRUD + control)."""
|
"""Sync clock schemas (CRUD + control)."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -11,14 +11,14 @@ class SyncClockCreate(BaseModel):
|
|||||||
|
|
||||||
name: str = Field(description="Clock name", min_length=1, max_length=100)
|
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)
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
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):
|
class SyncClockUpdate(BaseModel):
|
||||||
"""Request to update a synchronization clock."""
|
"""Request to update a synchronization clock."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Clock name", min_length=1, max_length=100)
|
name: str | None = 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)
|
speed: float | None = 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)
|
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
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")
|
id: str = Field(description="Clock ID")
|
||||||
name: str = Field(description="Clock name")
|
name: str = Field(description="Clock name")
|
||||||
speed: float = Field(description="Speed multiplier")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library.",
|
description="Icon id from the curated icon library.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon.",
|
description="Optional CSS color override for the icon.",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Capture template and engine schemas."""
|
"""Capture template and engine schemas."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -12,14 +12,14 @@ class TemplateCreate(BaseModel):
|
|||||||
name: str = Field(description="Template name", min_length=1, max_length=100)
|
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_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")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||||
@@ -29,17 +29,17 @@ class TemplateCreate(BaseModel):
|
|||||||
class TemplateUpdate(BaseModel):
|
class TemplateUpdate(BaseModel):
|
||||||
"""Request to update a template."""
|
"""Request to update a template."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
name: str | None = 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_type: str | None = Field(None, description="Capture engine type (mss, dxcam, wgc)")
|
||||||
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
engine_config: Dict | None = Field(None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
description: str | None = Field(None, description="Template description")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None, max_length=64, description="Icon id from the curated icon library."
|
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."
|
None, max_length=32, description="Optional CSS color override for the icon."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Value source schemas — discriminated unions per source type."""
|
"""Value source schemas — discriminated unions per source type."""
|
||||||
|
|
||||||
from datetime import datetime
|
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
|
from pydantic import BaseModel, Discriminator, Field, Tag
|
||||||
|
|
||||||
@@ -15,14 +15,14 @@ class _ValueSourceResponseBase(BaseModel):
|
|||||||
|
|
||||||
id: str = Field(description="Source ID")
|
id: str = Field(description="Source ID")
|
||||||
name: str = Field(description="Source name")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library.",
|
description="Icon id from the curated icon library.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon.",
|
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], ...]")
|
colors: List[List[int]] = Field(description="Color list [[R,G,B], ...]")
|
||||||
speed: float = Field(description="Cycles per minute (ignored when clock_id is set)")
|
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")
|
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)"
|
None, description="Optional sync clock ID for shared timing (overrides speed)"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -163,22 +163,20 @@ class HTTPValueSourceResponse(_ValueSourceResponseBase):
|
|||||||
|
|
||||||
|
|
||||||
ValueSourceResponse = Annotated[
|
ValueSourceResponse = Annotated[
|
||||||
Union[
|
Annotated[StaticValueSourceResponse, Tag("static")]
|
||||||
Annotated[StaticValueSourceResponse, Tag("static")],
|
| Annotated[AnimatedValueSourceResponse, Tag("animated")]
|
||||||
Annotated[AnimatedValueSourceResponse, Tag("animated")],
|
| Annotated[AudioValueSourceResponse, Tag("audio")]
|
||||||
Annotated[AudioValueSourceResponse, Tag("audio")],
|
| Annotated[AdaptiveTimeValueSourceResponse, Tag("adaptive_time")]
|
||||||
Annotated[AdaptiveTimeValueSourceResponse, Tag("adaptive_time")],
|
| Annotated[AdaptiveSceneValueSourceResponse, Tag("adaptive_scene")]
|
||||||
Annotated[AdaptiveSceneValueSourceResponse, Tag("adaptive_scene")],
|
| Annotated[DaylightValueSourceResponse, Tag("daylight")]
|
||||||
Annotated[DaylightValueSourceResponse, Tag("daylight")],
|
| Annotated[StaticColorValueSourceResponse, Tag("static_color")]
|
||||||
Annotated[StaticColorValueSourceResponse, Tag("static_color")],
|
| Annotated[AnimatedColorValueSourceResponse, Tag("animated_color")]
|
||||||
Annotated[AnimatedColorValueSourceResponse, Tag("animated_color")],
|
| Annotated[AdaptiveTimeColorValueSourceResponse, Tag("adaptive_time_color")]
|
||||||
Annotated[AdaptiveTimeColorValueSourceResponse, Tag("adaptive_time_color")],
|
| Annotated[HAEntityValueSourceResponse, Tag("ha_entity")]
|
||||||
Annotated[HAEntityValueSourceResponse, Tag("ha_entity")],
|
| Annotated[GradientMapValueSourceResponse, Tag("gradient_map")]
|
||||||
Annotated[GradientMapValueSourceResponse, Tag("gradient_map")],
|
| Annotated[CSSExtractValueSourceResponse, Tag("css_extract")]
|
||||||
Annotated[CSSExtractValueSourceResponse, Tag("css_extract")],
|
| Annotated[SystemMetricsValueSourceResponse, Tag("system_metrics")]
|
||||||
Annotated[SystemMetricsValueSourceResponse, Tag("system_metrics")],
|
| Annotated[HTTPValueSourceResponse, Tag("http")],
|
||||||
Annotated[HTTPValueSourceResponse, Tag("http")],
|
|
||||||
],
|
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -191,14 +189,14 @@ class _ValueSourceCreateBase(BaseModel):
|
|||||||
"""Shared fields for all value source create requests."""
|
"""Shared fields for all value source create requests."""
|
||||||
|
|
||||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
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(
|
easing: str = Field(
|
||||||
"linear", description="Color easing: linear|step|ease_in|ease_out|ease_in_out|sine"
|
"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)"
|
None, description="Optional sync clock ID (overrides speed when set)"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -333,22 +331,20 @@ class HTTPValueSourceCreate(_ValueSourceCreateBase):
|
|||||||
|
|
||||||
|
|
||||||
ValueSourceCreate = Annotated[
|
ValueSourceCreate = Annotated[
|
||||||
Union[
|
Annotated[StaticValueSourceCreate, Tag("static")]
|
||||||
Annotated[StaticValueSourceCreate, Tag("static")],
|
| Annotated[AnimatedValueSourceCreate, Tag("animated")]
|
||||||
Annotated[AnimatedValueSourceCreate, Tag("animated")],
|
| Annotated[AudioValueSourceCreate, Tag("audio")]
|
||||||
Annotated[AudioValueSourceCreate, Tag("audio")],
|
| Annotated[AdaptiveTimeValueSourceCreate, Tag("adaptive_time")]
|
||||||
Annotated[AdaptiveTimeValueSourceCreate, Tag("adaptive_time")],
|
| Annotated[AdaptiveSceneValueSourceCreate, Tag("adaptive_scene")]
|
||||||
Annotated[AdaptiveSceneValueSourceCreate, Tag("adaptive_scene")],
|
| Annotated[DaylightValueSourceCreate, Tag("daylight")]
|
||||||
Annotated[DaylightValueSourceCreate, Tag("daylight")],
|
| Annotated[StaticColorValueSourceCreate, Tag("static_color")]
|
||||||
Annotated[StaticColorValueSourceCreate, Tag("static_color")],
|
| Annotated[AnimatedColorValueSourceCreate, Tag("animated_color")]
|
||||||
Annotated[AnimatedColorValueSourceCreate, Tag("animated_color")],
|
| Annotated[AdaptiveTimeColorValueSourceCreate, Tag("adaptive_time_color")]
|
||||||
Annotated[AdaptiveTimeColorValueSourceCreate, Tag("adaptive_time_color")],
|
| Annotated[HAEntityValueSourceCreate, Tag("ha_entity")]
|
||||||
Annotated[HAEntityValueSourceCreate, Tag("ha_entity")],
|
| Annotated[GradientMapValueSourceCreate, Tag("gradient_map")]
|
||||||
Annotated[GradientMapValueSourceCreate, Tag("gradient_map")],
|
| Annotated[CSSExtractValueSourceCreate, Tag("css_extract")]
|
||||||
Annotated[CSSExtractValueSourceCreate, Tag("css_extract")],
|
| Annotated[SystemMetricsValueSourceCreate, Tag("system_metrics")]
|
||||||
Annotated[SystemMetricsValueSourceCreate, Tag("system_metrics")],
|
| Annotated[HTTPValueSourceCreate, Tag("http")],
|
||||||
Annotated[HTTPValueSourceCreate, Tag("http")],
|
|
||||||
],
|
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -360,15 +356,15 @@ ValueSourceCreate = Annotated[
|
|||||||
class _ValueSourceUpdateBase(BaseModel):
|
class _ValueSourceUpdateBase(BaseModel):
|
||||||
"""Shared fields for all value source update requests."""
|
"""Shared fields for all value source update requests."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
name: str | None = Field(None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
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):
|
class StaticValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["static"] = "static"
|
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):
|
class AnimatedValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["animated"] = "animated"
|
source_type: Literal["animated"] = "animated"
|
||||||
waveform: Optional[str] = Field(None, description="Waveform: sine|triangle|square|sawtooth")
|
waveform: str | None = Field(None, description="Waveform: sine|triangle|square|sawtooth")
|
||||||
speed: Optional[float] = Field(None, description="Cycles per minute", ge=0.1, le=120.0)
|
speed: float | None = 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)
|
min_value: float | None = 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)
|
max_value: float | None = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
class AudioValueSourceUpdate(_ValueSourceUpdateBase):
|
class AudioValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["audio"] = "audio"
|
source_type: Literal["audio"] = "audio"
|
||||||
audio_source_id: Optional[str] = Field(None, description="Mono audio source ID")
|
audio_source_id: str | None = Field(None, description="Mono audio source ID")
|
||||||
mode: Optional[str] = Field(None, description="Audio mode: rms|peak|beat")
|
mode: str | None = Field(None, description="Audio mode: rms|peak|beat")
|
||||||
sensitivity: Optional[float] = Field(None, description="Gain multiplier", ge=0.1, le=20.0)
|
sensitivity: float | None = 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)
|
smoothing: float | None = 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)
|
min_value: float | None = 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)
|
max_value: float | None = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||||
auto_gain: Optional[bool] = Field(None, description="Auto-normalize audio levels")
|
auto_gain: bool | None = Field(None, description="Auto-normalize audio levels")
|
||||||
|
|
||||||
|
|
||||||
class AdaptiveTimeValueSourceUpdate(_ValueSourceUpdateBase):
|
class AdaptiveTimeValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["adaptive_time"] = "adaptive_time"
|
source_type: Literal["adaptive_time"] = "adaptive_time"
|
||||||
schedule: Optional[list] = Field(None, description="Time-of-day schedule")
|
schedule: list | None = Field(None, description="Time-of-day schedule")
|
||||||
min_value: Optional[float] = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
min_value: float | None = 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)
|
max_value: float | None = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
class AdaptiveSceneValueSourceUpdate(_ValueSourceUpdateBase):
|
class AdaptiveSceneValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["adaptive_scene"] = "adaptive_scene"
|
source_type: Literal["adaptive_scene"] = "adaptive_scene"
|
||||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
picture_source_id: str | None = Field(None, description="Picture source ID")
|
||||||
scene_behavior: Optional[str] = Field(None, description="Scene behavior")
|
scene_behavior: str | None = Field(None, description="Scene behavior")
|
||||||
sensitivity: Optional[float] = Field(None, description="Gain multiplier", ge=0.1, le=20.0)
|
sensitivity: float | None = 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)
|
smoothing: float | None = 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)
|
min_value: float | None = 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)
|
max_value: float | None = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
class DaylightValueSourceUpdate(_ValueSourceUpdateBase):
|
class DaylightValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["daylight"] = "daylight"
|
source_type: Literal["daylight"] = "daylight"
|
||||||
speed: Optional[float] = Field(None, description="Simulation speed", ge=0.1, le=120.0)
|
speed: float | None = Field(None, description="Simulation speed", ge=0.1, le=120.0)
|
||||||
use_real_time: Optional[bool] = Field(None, description="Use wall-clock time")
|
use_real_time: bool | None = Field(None, description="Use wall-clock time")
|
||||||
latitude: Optional[float] = Field(None, description="Geographic latitude", ge=-90.0, le=90.0)
|
latitude: float | None = Field(None, description="Geographic latitude", ge=-90.0, le=90.0)
|
||||||
longitude: Optional[float] = Field(
|
longitude: float | None = Field(None, description="Geographic longitude", ge=-180.0, le=180.0)
|
||||||
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)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class StaticColorValueSourceUpdate(_ValueSourceUpdateBase):
|
class StaticColorValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["static_color"] = "static_color"
|
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):
|
class AnimatedColorValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["animated_color"] = "animated_color"
|
source_type: Literal["animated_color"] = "animated_color"
|
||||||
colors: Optional[List[List[int]]] = Field(None, description="Color list [[R,G,B], ...]")
|
colors: List[List[int]] | None = Field(None, description="Color list [[R,G,B], ...]")
|
||||||
speed: Optional[float] = Field(None, description="Cycles per minute", ge=0.1, le=120.0)
|
speed: float | None = Field(None, description="Cycles per minute", ge=0.1, le=120.0)
|
||||||
easing: Optional[str] = Field(
|
easing: str | None = Field(
|
||||||
None, description="Color easing: linear|step|ease_in|ease_out|ease_in_out|sine"
|
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)"
|
None, description="Optional sync clock ID (empty string clears, null leaves unchanged)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdaptiveTimeColorValueSourceUpdate(_ValueSourceUpdateBase):
|
class AdaptiveTimeColorValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["adaptive_time_color"] = "adaptive_time_color"
|
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):
|
class HAEntityValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["ha_entity"] = "ha_entity"
|
source_type: Literal["ha_entity"] = "ha_entity"
|
||||||
ha_source_id: Optional[str] = Field(None, description="Home Assistant source ID")
|
ha_source_id: str | None = Field(None, description="Home Assistant source ID")
|
||||||
entity_id: Optional[str] = Field(None, description="HA entity ID")
|
entity_id: str | None = Field(None, description="HA entity ID")
|
||||||
attribute: Optional[str] = Field(None, description="Attribute name")
|
attribute: str | None = Field(None, description="Attribute name")
|
||||||
min_ha_value: Optional[float] = Field(None, description="Min HA value")
|
min_ha_value: float | None = Field(None, description="Min HA value")
|
||||||
max_ha_value: Optional[float] = Field(None, description="Max HA value")
|
max_ha_value: float | None = Field(None, description="Max HA value")
|
||||||
smoothing: Optional[float] = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
smoothing: float | None = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
class GradientMapValueSourceUpdate(_ValueSourceUpdateBase):
|
class GradientMapValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["gradient_map"] = "gradient_map"
|
source_type: Literal["gradient_map"] = "gradient_map"
|
||||||
value_source_id: Optional[str] = Field(None, description="Input value source ID")
|
value_source_id: str | None = Field(None, description="Input value source ID")
|
||||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||||
easing: Optional[str] = Field(None, description="Interpolation mode")
|
easing: str | None = Field(None, description="Interpolation mode")
|
||||||
|
|
||||||
|
|
||||||
class CSSExtractValueSourceUpdate(_ValueSourceUpdateBase):
|
class CSSExtractValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["css_extract"] = "css_extract"
|
source_type: Literal["css_extract"] = "css_extract"
|
||||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
color_strip_source_id: str | None = Field(None, description="Color strip source ID")
|
||||||
led_start: Optional[int] = Field(None, description="LED range start", ge=0)
|
led_start: int | None = Field(None, description="LED range start", ge=0)
|
||||||
led_end: Optional[int] = Field(None, description="LED range end")
|
led_end: int | None = Field(None, description="LED range end")
|
||||||
|
|
||||||
|
|
||||||
class SystemMetricsValueSourceUpdate(_ValueSourceUpdateBase):
|
class SystemMetricsValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["system_metrics"] = "system_metrics"
|
source_type: Literal["system_metrics"] = "system_metrics"
|
||||||
metric: Optional[str] = Field(None, description="System metric")
|
metric: str | None = Field(None, description="System metric")
|
||||||
min_value: Optional[float] = Field(None, description="Min value")
|
min_value: float | None = Field(None, description="Min value")
|
||||||
max_value: Optional[float] = Field(None, description="Max value")
|
max_value: float | None = Field(None, description="Max value")
|
||||||
max_rate: Optional[float] = Field(None, description="Max rate bytes/sec")
|
max_rate: float | None = Field(None, description="Max rate bytes/sec")
|
||||||
disk_path: Optional[str] = Field(None, description="Disk path")
|
disk_path: str | None = Field(None, description="Disk path")
|
||||||
sensor_label: Optional[str] = Field(None, description="Sensor label")
|
sensor_label: str | None = Field(None, description="Sensor label")
|
||||||
poll_interval: Optional[float] = Field(None, description="Poll interval", ge=0.1, le=60.0)
|
poll_interval: float | None = 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)
|
smoothing: float | None = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
class HTTPValueSourceUpdate(_ValueSourceUpdateBase):
|
class HTTPValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
source_type: Literal["http"] = "http"
|
source_type: Literal["http"] = "http"
|
||||||
http_endpoint_id: Optional[str] = Field(None, description="HTTP endpoint ID")
|
http_endpoint_id: str | None = Field(None, description="HTTP endpoint ID")
|
||||||
json_path: Optional[str] = Field(None, description="Dot-path into the response")
|
json_path: str | None = Field(None, description="Dot-path into the response")
|
||||||
interval_s: Optional[int] = Field(None, description="Polling cadence (seconds)", ge=1)
|
interval_s: int | None = Field(None, description="Polling cadence (seconds)", ge=1)
|
||||||
min_value: Optional[float] = Field(None, description="Raw value mapped to 0.0")
|
min_value: float | None = Field(None, description="Raw value mapped to 0.0")
|
||||||
max_value: Optional[float] = Field(None, description="Raw value mapped to 1.0")
|
max_value: float | None = Field(None, description="Raw value mapped to 1.0")
|
||||||
smoothing: Optional[float] = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
smoothing: float | None = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||||
|
|
||||||
|
|
||||||
ValueSourceUpdate = Annotated[
|
ValueSourceUpdate = Annotated[
|
||||||
Union[
|
Annotated[StaticValueSourceUpdate, Tag("static")]
|
||||||
Annotated[StaticValueSourceUpdate, Tag("static")],
|
| Annotated[AnimatedValueSourceUpdate, Tag("animated")]
|
||||||
Annotated[AnimatedValueSourceUpdate, Tag("animated")],
|
| Annotated[AudioValueSourceUpdate, Tag("audio")]
|
||||||
Annotated[AudioValueSourceUpdate, Tag("audio")],
|
| Annotated[AdaptiveTimeValueSourceUpdate, Tag("adaptive_time")]
|
||||||
Annotated[AdaptiveTimeValueSourceUpdate, Tag("adaptive_time")],
|
| Annotated[AdaptiveSceneValueSourceUpdate, Tag("adaptive_scene")]
|
||||||
Annotated[AdaptiveSceneValueSourceUpdate, Tag("adaptive_scene")],
|
| Annotated[DaylightValueSourceUpdate, Tag("daylight")]
|
||||||
Annotated[DaylightValueSourceUpdate, Tag("daylight")],
|
| Annotated[StaticColorValueSourceUpdate, Tag("static_color")]
|
||||||
Annotated[StaticColorValueSourceUpdate, Tag("static_color")],
|
| Annotated[AnimatedColorValueSourceUpdate, Tag("animated_color")]
|
||||||
Annotated[AnimatedColorValueSourceUpdate, Tag("animated_color")],
|
| Annotated[AdaptiveTimeColorValueSourceUpdate, Tag("adaptive_time_color")]
|
||||||
Annotated[AdaptiveTimeColorValueSourceUpdate, Tag("adaptive_time_color")],
|
| Annotated[HAEntityValueSourceUpdate, Tag("ha_entity")]
|
||||||
Annotated[HAEntityValueSourceUpdate, Tag("ha_entity")],
|
| Annotated[GradientMapValueSourceUpdate, Tag("gradient_map")]
|
||||||
Annotated[GradientMapValueSourceUpdate, Tag("gradient_map")],
|
| Annotated[CSSExtractValueSourceUpdate, Tag("css_extract")]
|
||||||
Annotated[CSSExtractValueSourceUpdate, Tag("css_extract")],
|
| Annotated[SystemMetricsValueSourceUpdate, Tag("system_metrics")]
|
||||||
Annotated[SystemMetricsValueSourceUpdate, Tag("system_metrics")],
|
| Annotated[HTTPValueSourceUpdate, Tag("http")],
|
||||||
Annotated[HTTPValueSourceUpdate, Tag("http")],
|
|
||||||
],
|
|
||||||
Discriminator("source_type"),
|
Discriminator("source_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Weather source schemas (CRUD)."""
|
"""Weather source schemas (CRUD)."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Literal, Optional
|
from typing import Dict, List, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class WeatherSourceCreate(BaseModel):
|
|||||||
provider: Literal["open_meteo"] = Field(
|
provider: Literal["open_meteo"] = Field(
|
||||||
default="open_meteo", description="Weather data provider"
|
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(
|
latitude: float = Field(
|
||||||
default=50.0, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0
|
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(
|
update_interval: int = Field(
|
||||||
default=600, description="API poll interval in seconds (60-3600)", ge=60, le=3600
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
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):
|
class WeatherSourceUpdate(BaseModel):
|
||||||
"""Request to update a weather source."""
|
"""Request to update a weather source."""
|
||||||
|
|
||||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
name: str | None = Field(None, description="Source name", min_length=1, max_length=100)
|
||||||
provider: Optional[Literal["open_meteo"]] = Field(None, description="Weather data provider")
|
provider: Literal["open_meteo"] | None = Field(None, 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: Optional[float] = Field(
|
latitude: float | None = Field(
|
||||||
None, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0
|
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
|
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
|
None, 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: Optional[List[str]] = None
|
tags: List[str] | None = None
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
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")
|
latitude: float = Field(description="Geographic latitude")
|
||||||
longitude: float = Field(description="Geographic longitude")
|
longitude: float = Field(description="Geographic longitude")
|
||||||
update_interval: int = Field(description="API poll interval in seconds")
|
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")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
icon: Optional[str] = Field(
|
icon: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=64,
|
max_length=64,
|
||||||
description="Icon id from the curated icon library.",
|
description="Icon id from the curated icon library.",
|
||||||
)
|
)
|
||||||
icon_color: Optional[str] = Field(
|
icon_color: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
description="Optional CSS color override for the icon.",
|
description="Optional CSS color override for the icon.",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ capture stream (WASAPI, sounddevice, etc.).
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
from ledgrab.core.audio.analysis import (
|
from ledgrab.core.audio.analysis import (
|
||||||
AudioAnalysis,
|
AudioAnalysis,
|
||||||
@@ -49,7 +49,7 @@ class ManagedAudioStream:
|
|||||||
engine_type: str,
|
engine_type: str,
|
||||||
device_index: int,
|
device_index: int,
|
||||||
is_loopback: bool,
|
is_loopback: bool,
|
||||||
engine_config: Optional[Dict[str, Any]] = None,
|
engine_config: Dict[str, Any] | None = None,
|
||||||
):
|
):
|
||||||
self._engine_type = engine_type
|
self._engine_type = engine_type
|
||||||
self._device_index = device_index
|
self._device_index = device_index
|
||||||
@@ -57,9 +57,9 @@ class ManagedAudioStream:
|
|||||||
self._engine_config = engine_config or {}
|
self._engine_config = engine_config or {}
|
||||||
|
|
||||||
self._running = False
|
self._running = False
|
||||||
self._thread: Optional[threading.Thread] = None
|
self._thread: threading.Thread | None = None
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._latest: Optional[AudioAnalysis] = None
|
self._latest: AudioAnalysis | None = None
|
||||||
self._last_timing: dict = {}
|
self._last_timing: dict = {}
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
@@ -90,7 +90,7 @@ class ManagedAudioStream:
|
|||||||
f"device={self._device_index}"
|
f"device={self._device_index}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_latest_analysis(self) -> Optional[AudioAnalysis]:
|
def get_latest_analysis(self) -> AudioAnalysis | None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
return self._latest
|
return self._latest
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ class ManagedAudioStream:
|
|||||||
return dict(self._last_timing)
|
return dict(self._last_timing)
|
||||||
|
|
||||||
def _capture_loop(self) -> None:
|
def _capture_loop(self) -> None:
|
||||||
stream: Optional[AudioCaptureStreamBase] = None
|
stream: AudioCaptureStreamBase | None = None
|
||||||
try:
|
try:
|
||||||
stream = AudioEngineRegistry.create_stream(
|
stream = AudioEngineRegistry.create_stream(
|
||||||
self._engine_type,
|
self._engine_type,
|
||||||
@@ -178,8 +178,8 @@ class AudioCaptureManager:
|
|||||||
self,
|
self,
|
||||||
device_index: int,
|
device_index: int,
|
||||||
is_loopback: bool,
|
is_loopback: bool,
|
||||||
engine_type: Optional[str] = None,
|
engine_type: str | None = None,
|
||||||
engine_config: Optional[Dict[str, Any]] = None,
|
engine_config: Dict[str, Any] | None = None,
|
||||||
) -> ManagedAudioStream:
|
) -> ManagedAudioStream:
|
||||||
"""Get or create a ManagedAudioStream for the given device.
|
"""Get or create a ManagedAudioStream for the given device.
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ class AudioCaptureManager:
|
|||||||
self,
|
self,
|
||||||
device_index: int,
|
device_index: int,
|
||||||
is_loopback: bool,
|
is_loopback: bool,
|
||||||
engine_type: Optional[str] = None,
|
engine_type: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Release a reference to a ManagedAudioStream."""
|
"""Release a reference to a ManagedAudioStream."""
|
||||||
if engine_type is None:
|
if engine_type is None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class AudioCaptureStreamBase(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def read_chunk(self) -> Optional[np.ndarray]:
|
def read_chunk(self) -> np.ndarray | None:
|
||||||
"""Read one chunk of raw audio data.
|
"""Read one chunk of raw audio data.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Demo audio engine — virtual audio devices with synthetic audio data."""
|
"""Demo audio engine — virtual audio devices with synthetic audio data."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class DemoAudioCaptureStream(AudioCaptureStreamBase):
|
|||||||
self._initialized = False
|
self._initialized = False
|
||||||
logger.info(f"Demo audio stream cleaned up (device={self.device_index})")
|
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:
|
if not self._initialized:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Engine registry and factory for audio capture engines."""
|
"""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.core.audio.base import AudioCaptureEngine, AudioCaptureStreamBase
|
||||||
from ledgrab.config import is_demo_mode
|
from ledgrab.config import is_demo_mode
|
||||||
@@ -82,7 +82,7 @@ class AudioEngineRegistry:
|
|||||||
return available
|
return available
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_best_available_engine(cls) -> Optional[str]:
|
def get_best_available_engine(cls) -> str | None:
|
||||||
"""Get the highest-priority available engine type.
|
"""Get the highest-priority available engine type.
|
||||||
|
|
||||||
Returns:
|
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.filters.registry import AudioFilterRegistry
|
||||||
from ledgrab.core.audio.band_filter import apply_band_filter, compute_band_mask
|
from ledgrab.core.audio.band_filter import apply_band_filter, compute_band_mask
|
||||||
|
|
||||||
|
|
||||||
# Preset frequency ranges
|
# Preset frequency ranges
|
||||||
_PRESETS = {
|
_PRESETS = {
|
||||||
"bass": (20.0, 250.0),
|
"bass": (20.0, 250.0),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ class AudioFilterOptionDef:
|
|||||||
min_value: Any
|
min_value: Any
|
||||||
max_value: Any
|
max_value: Any
|
||||||
step: Any
|
step: Any
|
||||||
choices: Optional[List[Dict[str, str]]] = None # for "select": [{value, label}]
|
choices: List[Dict[str, str]] | None = None # for "select": [{value, label}]
|
||||||
max_length: Optional[int] = None # for "string" type
|
max_length: int | None = None # for "string" type
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
d = {
|
d = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Sounddevice audio capture engine (cross-platform, via PortAudio)."""
|
"""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
|
import numpy as np
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class SounddeviceCaptureStream(AudioCaptureStreamBase):
|
|||||||
self._sd_stream = None
|
self._sd_stream = None
|
||||||
self._initialized = False
|
self._initialized = False
|
||||||
|
|
||||||
def read_chunk(self) -> Optional[np.ndarray]:
|
def read_chunk(self) -> np.ndarray | None:
|
||||||
if self._sd_stream is None:
|
if self._sd_stream is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""WASAPI audio capture engine (Windows only, via PyAudioWPatch)."""
|
"""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
|
import numpy as np
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ class WasapiCaptureStream(AudioCaptureStreamBase):
|
|||||||
self._pa = None
|
self._pa = None
|
||||||
self._initialized = False
|
self._initialized = False
|
||||||
|
|
||||||
def read_chunk(self) -> Optional[np.ndarray]:
|
def read_chunk(self) -> np.ndarray | None:
|
||||||
if self._stream is None:
|
if self._stream is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
@@ -109,7 +109,7 @@ class WasapiCaptureStream(AudioCaptureStreamBase):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@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."""
|
"""Find the PyAudioWPatch loopback device for a given output device."""
|
||||||
try:
|
try:
|
||||||
first_loopback = None
|
first_loopback = None
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
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.core.automations.platform_detector import PlatformDetector
|
||||||
from ledgrab.storage.automation import (
|
from ledgrab.storage.automation import (
|
||||||
@@ -38,11 +38,11 @@ class _RuleEvalContext:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
running_procs: Set[str]
|
running_procs: Set[str]
|
||||||
topmost_proc: Optional[str]
|
topmost_proc: str | None
|
||||||
topmost_fullscreen: bool
|
topmost_fullscreen: bool
|
||||||
fullscreen_procs: Set[str]
|
fullscreen_procs: Set[str]
|
||||||
idle_seconds: Optional[float]
|
idle_seconds: float | None
|
||||||
display_state: Optional[str]
|
display_state: str | None
|
||||||
|
|
||||||
|
|
||||||
def _apply_operator(operator: str, extracted, expected: str) -> bool:
|
def _apply_operator(operator: str, extracted, expected: str) -> bool:
|
||||||
@@ -101,7 +101,7 @@ class AutomationEngine:
|
|||||||
self._target_store = target_store
|
self._target_store = target_store
|
||||||
self._device_store = device_store
|
self._device_store = device_store
|
||||||
self._ha_manager = ha_manager
|
self._ha_manager = ha_manager
|
||||||
self._task: Optional[asyncio.Task] = None
|
self._task: asyncio.Task | None = None
|
||||||
self._eval_lock = asyncio.Lock()
|
self._eval_lock = asyncio.Lock()
|
||||||
|
|
||||||
# Runtime state (not persisted)
|
# Runtime state (not persisted)
|
||||||
@@ -420,11 +420,11 @@ class AutomationEngine:
|
|||||||
self,
|
self,
|
||||||
automation: Automation,
|
automation: Automation,
|
||||||
running_procs: Set[str],
|
running_procs: Set[str],
|
||||||
topmost_proc: Optional[str],
|
topmost_proc: str | None,
|
||||||
topmost_fullscreen: bool,
|
topmost_fullscreen: bool,
|
||||||
fullscreen_procs: Set[str],
|
fullscreen_procs: Set[str],
|
||||||
idle_seconds: Optional[float],
|
idle_seconds: float | None,
|
||||||
display_state: Optional[str],
|
display_state: str | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
results = [
|
results = [
|
||||||
self._evaluate_rule(
|
self._evaluate_rule(
|
||||||
@@ -453,11 +453,11 @@ class AutomationEngine:
|
|||||||
self,
|
self,
|
||||||
rule: Rule,
|
rule: Rule,
|
||||||
running_procs: Set[str],
|
running_procs: Set[str],
|
||||||
topmost_proc: Optional[str],
|
topmost_proc: str | None,
|
||||||
topmost_fullscreen: bool,
|
topmost_fullscreen: bool,
|
||||||
fullscreen_procs: Set[str],
|
fullscreen_procs: Set[str],
|
||||||
idle_seconds: Optional[float],
|
idle_seconds: float | None,
|
||||||
display_state: Optional[str],
|
display_state: str | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
ctx = _RuleEvalContext(
|
ctx = _RuleEvalContext(
|
||||||
running_procs=running_procs,
|
running_procs=running_procs,
|
||||||
@@ -531,14 +531,14 @@ class AutomationEngine:
|
|||||||
return current >= start or current <= end
|
return current >= start or current <= end
|
||||||
|
|
||||||
@staticmethod
|
@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:
|
if idle_seconds is None:
|
||||||
return False
|
return False
|
||||||
is_idle = idle_seconds >= (rule.idle_minutes * 60)
|
is_idle = idle_seconds >= (rule.idle_minutes * 60)
|
||||||
return is_idle if rule.when_idle else not is_idle
|
return is_idle if rule.when_idle else not is_idle
|
||||||
|
|
||||||
@staticmethod
|
@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:
|
if display_state is None:
|
||||||
return False
|
return False
|
||||||
return display_state == rule.state
|
return display_state == rule.state
|
||||||
@@ -612,7 +612,7 @@ class AutomationEngine:
|
|||||||
self,
|
self,
|
||||||
rule: ApplicationRule,
|
rule: ApplicationRule,
|
||||||
running_procs: Set[str],
|
running_procs: Set[str],
|
||||||
topmost_proc: Optional[str],
|
topmost_proc: str | None,
|
||||||
topmost_fullscreen: bool,
|
topmost_fullscreen: bool,
|
||||||
fullscreen_procs: Set[str],
|
fullscreen_procs: Set[str],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ctypes
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from typing import Optional, Set
|
from typing import Set
|
||||||
|
|
||||||
from ledgrab.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ class PlatformDetector:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Display power listener failed: {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."""
|
"""Get display power state: 'on' or 'off'. Returns None if unavailable."""
|
||||||
if not _IS_WINDOWS:
|
if not _IS_WINDOWS:
|
||||||
return None
|
return None
|
||||||
@@ -172,7 +172,7 @@ class PlatformDetector:
|
|||||||
|
|
||||||
# ---- System idle detection ----
|
# ---- 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).
|
"""Get system idle time in seconds (keyboard/mouse inactivity).
|
||||||
|
|
||||||
Returns None if detection is unavailable.
|
Returns None if detection is unavailable.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from ledgrab.storage.database import Database
|
from ledgrab.storage.database import Database
|
||||||
from ledgrab.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
@@ -33,8 +33,8 @@ class AutoBackupEngine:
|
|||||||
):
|
):
|
||||||
self._backup_dir = Path(backup_dir)
|
self._backup_dir = Path(backup_dir)
|
||||||
self._db = db
|
self._db = db
|
||||||
self._task: Optional[asyncio.Task] = None
|
self._task: asyncio.Task | None = None
|
||||||
self._last_backup_time: Optional[datetime] = None
|
self._last_backup_time: datetime | None = None
|
||||||
|
|
||||||
self._settings = self._load_settings()
|
self._settings = self._load_settings()
|
||||||
self._backup_dir.mkdir(parents=True, exist_ok=True)
|
self._backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"""Pixel processing utilities for color correction and manipulation."""
|
"""Pixel processing utilities for color correction and manipulation."""
|
||||||
|
|
||||||
from typing import List, Tuple, Union
|
from typing import List, Tuple
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from ledgrab.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
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:
|
def _as_array(colors: ColorList) -> np.ndarray:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import colorsys
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
from typing import TYPE_CHECKING, Dict, List
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
@@ -41,8 +41,8 @@ class OverlayWindow:
|
|||||||
self.calibration = calibration
|
self.calibration = calibration
|
||||||
self.target_id = target_id
|
self.target_id = target_id
|
||||||
self.target_name = target_name or target_id
|
self.target_name = target_name or target_id
|
||||||
self._window: Optional[tk.Toplevel] = None
|
self._window: tk.Toplevel | None = None
|
||||||
self._canvas: Optional[tk.Canvas] = None
|
self._canvas: tk.Canvas | None = None
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
# ----- Lifecycle (must run in Tk thread) -----
|
# ----- Lifecycle (must run in Tk thread) -----
|
||||||
@@ -352,8 +352,8 @@ class OverlayManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._overlays: Dict[str, OverlayWindow] = {}
|
self._overlays: Dict[str, OverlayWindow] = {}
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._tk_root: Optional[tk.Tk] = None
|
self._tk_root: tk.Tk | None = None
|
||||||
self._tk_thread: Optional[threading.Thread] = None
|
self._tk_thread: threading.Thread | None = None
|
||||||
self._tk_ready = threading.Event()
|
self._tk_ready = threading.Event()
|
||||||
self._start_tk_thread()
|
self._start_tk_thread()
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ class OverlayManager:
|
|||||||
if self._tk_root is None:
|
if self._tk_root is None:
|
||||||
raise RuntimeError("Tkinter root not available")
|
raise RuntimeError("Tkinter root not available")
|
||||||
done = threading.Event()
|
done = threading.Event()
|
||||||
exc_box: List[Optional[BaseException]] = [None]
|
exc_box: List[BaseException | None] = [None]
|
||||||
|
|
||||||
def wrapper():
|
def wrapper():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class CaptureStream(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
def capture_frame(self) -> ScreenCapture | None:
|
||||||
"""Capture one frame from the bound display.
|
"""Capture one frame from the bound display.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
from ledgrab.core.capture_engines.base import (
|
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}")
|
logger.error(f"BetterCam reinit failed (display={self.display_index}): {reinit_err}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
def capture_frame(self) -> ScreenCapture | None:
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import platform
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
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
|
# OpenCV's MSMF backend on Windows often fails to open the device
|
||||||
# ("cap.isOpened() == False" right after VideoCapture returns) when
|
# ("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."""
|
"""Parse a 'WxH' string into (width, height). Returns None for 'auto' or invalid."""
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
return None
|
return None
|
||||||
@@ -101,7 +101,7 @@ _BUILDINFO_LABELS: Dict[str, str] = {
|
|||||||
"avfoundation": "AVFoundation",
|
"avfoundation": "AVFoundation",
|
||||||
}
|
}
|
||||||
|
|
||||||
_compiled_backends_cache: Optional[Set[str]] = None
|
_compiled_backends_cache: Set[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _get_compiled_backends() -> Set[str]:
|
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)]
|
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."""
|
"""Convert a backend name string to cv2 API preference constant."""
|
||||||
return _CV2_BACKENDS.get(backend_name)
|
return _CV2_BACKENDS.get(backend_name)
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ def _get_camera_friendly_names() -> Dict[int, str]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
_camera_cache: Optional[List[Dict[str, Any]]] = None
|
_camera_cache: List[Dict[str, Any]] | None = None
|
||||||
_camera_cache_time: float = 0
|
_camera_cache_time: float = 0
|
||||||
_CAMERA_CACHE_TTL = 30.0 # seconds
|
_CAMERA_CACHE_TTL = 30.0 # seconds
|
||||||
|
|
||||||
@@ -428,7 +428,7 @@ class CameraCaptureStream(CaptureStream):
|
|||||||
def __init__(self, display_index: int, config: Dict[str, Any]):
|
def __init__(self, display_index: int, config: Dict[str, Any]):
|
||||||
super().__init__(display_index, config)
|
super().__init__(display_index, config)
|
||||||
self._cap = None
|
self._cap = None
|
||||||
self._cv2_index: Optional[int] = None
|
self._cv2_index: int | None = None
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
if self._initialized:
|
if self._initialized:
|
||||||
@@ -531,7 +531,7 @@ class CameraCaptureStream(CaptureStream):
|
|||||||
f"(camera={camera['name']}, cv2_idx={cv2_index}, {w}x{h})"
|
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:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Demo capture engine — virtual displays with animated test patterns."""
|
"""Demo capture engine — virtual displays with animated test patterns."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class DemoCaptureStream(CaptureStream):
|
|||||||
self._initialized = False
|
self._initialized = False
|
||||||
logger.info(f"Demo capture stream cleaned up (display={self.display_index})")
|
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:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
from ledgrab.core.capture_engines.base import (
|
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}")
|
logger.error(f"DXcam reinit failed (display={self.display_index}): {reinit_err}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
def capture_frame(self) -> ScreenCapture | None:
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Engine registry and factory for screen capture engines."""
|
"""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.core.capture_engines.base import CaptureEngine, CaptureStream
|
||||||
from ledgrab.config import is_demo_mode
|
from ledgrab.config import is_demo_mode
|
||||||
@@ -83,7 +83,7 @@ class EngineRegistry:
|
|||||||
return available
|
return available
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_best_available_engine(cls) -> Optional[str]:
|
def get_best_available_engine(cls) -> str | None:
|
||||||
"""Get the highest-priority available engine type.
|
"""Get the highest-priority available engine type.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ logger = get_logger(__name__)
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_frame_queue: queue.Queue["ScreenCapture"] = queue.Queue(maxsize=2)
|
_frame_queue: queue.Queue["ScreenCapture"] = queue.Queue(maxsize=2)
|
||||||
_display_info: Optional[DisplayInfo] = None
|
_display_info: DisplayInfo | None = None
|
||||||
_active = False
|
_active = False
|
||||||
_frames_received = 0
|
_frames_received = 0
|
||||||
_frames_consumed = 0
|
_frames_consumed = 0
|
||||||
@@ -141,7 +141,7 @@ class MediaProjectionCaptureStream(CaptureStream):
|
|||||||
self._initialized = True
|
self._initialized = True
|
||||||
logger.info("MediaProjection capture stream initialized")
|
logger.info("MediaProjection capture stream initialized")
|
||||||
|
|
||||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
def capture_frame(self) -> ScreenCapture | None:
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
# Prefer fresh frames from the queue; fall back to the last
|
# Prefer fresh frames from the queue; fall back to the last
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""MSS-based screen capture engine (cross-platform)."""
|
"""MSS-based screen capture engine (cross-platform)."""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import mss
|
import mss
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -41,7 +41,7 @@ class MSSCaptureStream(CaptureStream):
|
|||||||
self._rgb_idx: int = 0
|
self._rgb_idx: int = 0
|
||||||
self._rgb_shape: tuple = (0, 0)
|
self._rgb_shape: tuple = (0, 0)
|
||||||
# Cheap hash of the previous .raw bytes, for change detection.
|
# 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:
|
def initialize(self) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -59,7 +59,7 @@ class MSSCaptureStream(CaptureStream):
|
|||||||
self._prev_hash = None
|
self._prev_hash = None
|
||||||
logger.info(f"MSS capture stream cleaned up (display={self.display_index})")
|
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:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ logger = get_logger(__name__)
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_frame_queue: queue.Queue["ScreenCapture"] = queue.Queue(maxsize=2)
|
_frame_queue: queue.Queue["ScreenCapture"] = queue.Queue(maxsize=2)
|
||||||
_display_info: Optional[DisplayInfo] = None
|
_display_info: DisplayInfo | None = None
|
||||||
_active = False
|
_active = False
|
||||||
_frames_received = 0
|
_frames_received = 0
|
||||||
# screenrecord emits a full bitstream every frame (keyframes aside), so
|
# screenrecord emits a full bitstream every frame (keyframes aside), so
|
||||||
@@ -123,7 +123,7 @@ class RootScreenrecordCaptureStream(CaptureStream):
|
|||||||
self._initialized = True
|
self._initialized = True
|
||||||
logger.info("Root screenrecord capture stream initialized")
|
logger.info("Root screenrecord capture stream initialized")
|
||||||
|
|
||||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
def capture_frame(self) -> ScreenCapture | None:
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ video stream. No APK installation, no root.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -96,12 +96,12 @@ class ScrcpyClientCaptureStream(CaptureStream):
|
|||||||
|
|
||||||
def __init__(self, display_index: int, config: Dict[str, Any]):
|
def __init__(self, display_index: int, config: Dict[str, Any]):
|
||||||
super().__init__(display_index, config)
|
super().__init__(display_index, config)
|
||||||
self._client: Optional["scrcpy.Client"] = None
|
self._client: "scrcpy.Client" | None = None
|
||||||
self._latest_frame: Optional[ScreenCapture] = None
|
self._latest_frame: ScreenCapture | None = None
|
||||||
self._frame_lock = threading.Lock()
|
self._frame_lock = threading.Lock()
|
||||||
self._frame_event = threading.Event()
|
self._frame_event = threading.Event()
|
||||||
self._client_thread: Optional[threading.Thread] = None
|
self._client_thread: threading.Thread | None = None
|
||||||
self._device_serial: Optional[str] = None
|
self._device_serial: str | None = None
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
if self._initialized:
|
if self._initialized:
|
||||||
@@ -189,7 +189,7 @@ class ScrcpyClientCaptureStream(CaptureStream):
|
|||||||
)
|
)
|
||||||
self._frame_event.set()
|
self._frame_event.set()
|
||||||
|
|
||||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
def capture_frame(self) -> ScreenCapture | None:
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ def _find_adb() -> str:
|
|||||||
return "adb" # last resort — will fail with FileNotFoundError
|
return "adb" # last resort — will fail with FileNotFoundError
|
||||||
|
|
||||||
|
|
||||||
_adb_path: Optional[str] = None
|
_adb_path: str | None = None
|
||||||
|
|
||||||
|
|
||||||
def _get_adb() -> str:
|
def _get_adb() -> str:
|
||||||
@@ -158,7 +158,7 @@ def _list_adb_devices() -> List[Dict[str, Any]]:
|
|||||||
return devices
|
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."""
|
"""Capture a single PNG screenshot and return it as an RGB NumPy array."""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -190,12 +190,12 @@ class ScrcpyCaptureStream(CaptureStream):
|
|||||||
|
|
||||||
def __init__(self, display_index: int, config: Dict[str, Any]):
|
def __init__(self, display_index: int, config: Dict[str, Any]):
|
||||||
super().__init__(display_index, config)
|
super().__init__(display_index, config)
|
||||||
self._capture_thread: Optional[threading.Thread] = None
|
self._capture_thread: threading.Thread | None = None
|
||||||
self._latest_frame: Optional[ScreenCapture] = None
|
self._latest_frame: ScreenCapture | None = None
|
||||||
self._frame_lock = threading.Lock()
|
self._frame_lock = threading.Lock()
|
||||||
self._frame_event = threading.Event()
|
self._frame_event = threading.Event()
|
||||||
self._running = False
|
self._running = False
|
||||||
self._device_serial: Optional[str] = None
|
self._device_serial: str | None = None
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
if self._initialized:
|
if self._initialized:
|
||||||
@@ -281,7 +281,7 @@ class ScrcpyCaptureStream(CaptureStream):
|
|||||||
if poll_interval > 0:
|
if poll_interval > 0:
|
||||||
time.sleep(poll_interval)
|
time.sleep(poll_interval)
|
||||||
|
|
||||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
def capture_frame(self) -> ScreenCapture | None:
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import gc
|
import gc
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ class WGCCaptureStream(CaptureStream):
|
|||||||
gc.collect(0)
|
gc.collect(0)
|
||||||
logger.info(f"WGC capture stream cleaned up (display={self.display_index})")
|
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:
|
if not self._initialized:
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional, Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ def _build_adalight_header(led_count: int) -> bytes:
|
|||||||
class AdalightClient(LEDClient):
|
class AdalightClient(LEDClient):
|
||||||
"""LED client for Arduino Adalight serial devices."""
|
"""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.
|
"""Initialize Adalight client.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -62,11 +62,11 @@ class AdalightClient(LEDClient):
|
|||||||
# Pre-allocated wire buffer (header + RGB payload). Resized on the
|
# Pre-allocated wire buffer (header + RGB payload). Resized on the
|
||||||
# first frame and reused thereafter so the hot path performs no
|
# first frame and reused thereafter so the hot path performs no
|
||||||
# allocations — only a single memcpy of the pixel bytes.
|
# 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
|
self._frame_buf_n: int = 0
|
||||||
# Scratch uint8 array used to coerce non-uint8 / non-contiguous input
|
# Scratch uint8 array used to coerce non-uint8 / non-contiguous input
|
||||||
# without allocating a fresh array per frame.
|
# 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
|
self._u8_scratch_n: int = 0
|
||||||
# Dedicated single-worker executor for serial writes. Using
|
# Dedicated single-worker executor for serial writes. Using
|
||||||
# ``loop.run_in_executor`` against this avoids the per-call
|
# ``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
|
# that ``asyncio.to_thread`` incurs (~5–10 µs per call), and
|
||||||
# guarantees FIFO ordering of writes from this client even when
|
# guarantees FIFO ordering of writes from this client even when
|
||||||
# other tasks are using the default executor.
|
# 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:
|
async def connect(self) -> bool:
|
||||||
"""Open serial port and wait for Arduino reset."""
|
"""Open serial port and wait for Arduino reset."""
|
||||||
@@ -245,7 +245,7 @@ class AdalightClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Check if the serial port exists without opening it.
|
"""Check if the serial port exists without opening it.
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ so ``BLEClient`` can treat both backends identically.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from ledgrab.core.devices.ble_transport import DiscoveredBLEDevice
|
from ledgrab.core.devices.ble_transport import DiscoveredBLEDevice
|
||||||
from ledgrab.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
@@ -49,7 +49,7 @@ async def android_ble_scan(timeout: float = 4.0) -> List[DiscoveredBLEDevice]:
|
|||||||
continue
|
continue
|
||||||
address, name, rssi_str = parts
|
address, name, rssi_str = parts
|
||||||
try:
|
try:
|
||||||
rssi: Optional[int] = int(rssi_str)
|
rssi: int | None = int(rssi_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
rssi = None
|
rssi = None
|
||||||
devices.append(DiscoveredBLEDevice(address=address, name=name or address, rssi=rssi))
|
devices.append(DiscoveredBLEDevice(address=address, name=name or address, rssi=rssi))
|
||||||
@@ -80,7 +80,7 @@ class AndroidBLETransport:
|
|||||||
self._address = address
|
self._address = address
|
||||||
self._write_char_uuid = write_char_uuid
|
self._write_char_uuid = write_char_uuid
|
||||||
self._write_with_response = write_with_response
|
self._write_with_response = write_with_response
|
||||||
self._handle: Optional[int] = None
|
self._handle: int | None = None
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -120,7 +120,7 @@ class AndroidBLETransport:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Android BLE disconnect of %s raised: %s", self._address, 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.
|
"""Write bytes to a characteristic on the connected peripheral.
|
||||||
|
|
||||||
Serialised through an internal lock — BLE stacks do not tolerate
|
Serialised through an internal lock — BLE stacks do not tolerate
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ optional ``@baud`` suffix).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
from ledgrab.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from ledgrab.utils.platform import is_android
|
from ledgrab.utils.platform import is_android
|
||||||
@@ -38,7 +38,7 @@ def _bridge():
|
|||||||
class _UsbAddress:
|
class _UsbAddress:
|
||||||
vendor_id: int
|
vendor_id: int
|
||||||
product_id: int
|
product_id: int
|
||||||
serial: Optional[str]
|
serial: str | None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, device: str) -> "_UsbAddress":
|
def parse(cls, device: str) -> "_UsbAddress":
|
||||||
@@ -59,7 +59,7 @@ class _UsbAddress:
|
|||||||
return cls(vid, pid, serial)
|
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}"
|
base = f"usb:{vid:04x}:{pid:04x}"
|
||||||
return f"{base}:{serial}" if serial else base
|
return f"{base}:{serial}" if serial else base
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ class AndroidSerialTransport:
|
|||||||
self._url = device
|
self._url = device
|
||||||
self._addr = _UsbAddress.parse(device)
|
self._addr = _UsbAddress.parse(device)
|
||||||
self._baud_rate = baud_rate
|
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
|
@property
|
||||||
def is_open(self) -> bool:
|
def is_open(self) -> bool:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class BLEClient(LEDClient):
|
|||||||
write_with_response=self._protocol.write_with_response,
|
write_with_response=self._protocol.write_with_response,
|
||||||
)
|
)
|
||||||
# AES key for Govee encrypted firmware — 16 raw bytes or None.
|
# 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":
|
if ble_govee_key and ble_family == "govee":
|
||||||
try:
|
try:
|
||||||
import binascii
|
import binascii
|
||||||
@@ -104,7 +104,7 @@ class BLEClient(LEDClient):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Invalid Govee AES key — ignoring: %s", exc)
|
logger.warning("Invalid Govee AES key — ignoring: %s", exc)
|
||||||
self._last_write_at: float = 0.0
|
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
|
self._connected = False
|
||||||
# Throttle "not connected" warnings so the send loop doesn't spam logs
|
# Throttle "not connected" warnings so the send loop doesn't spam logs
|
||||||
# at frame rate when a BLE connection drops silently.
|
# at frame rate when a BLE connection drops silently.
|
||||||
@@ -161,12 +161,12 @@ class BLEClient(LEDClient):
|
|||||||
return self._connected and self._transport.is_connected
|
return self._connected and self._transport.is_connected
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
return self._led_count or None
|
return self._led_count or None
|
||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Average the strip to one color and write it — BLE protocols are whole-strip only."""
|
"""Average the strip to one color and write it — BLE protocols are whole-strip only."""
|
||||||
@@ -249,7 +249,7 @@ class BLEClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client, # noqa: ARG003 — unused; kept for the LEDClient contract
|
http_client, # noqa: ARG003 — unused; kept for the LEDClient contract
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""BLE health isn't a passive check — a full GATT connect is the only signal.
|
"""BLE health isn't a passive check — a full GATT connect is the only signal.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
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_client import BLEClient, _strip_ble_scheme
|
||||||
from ledgrab.core.devices.ble_protocols import (
|
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."""
|
"""Return the registered BLE provider, or ``None`` if not registered."""
|
||||||
from ledgrab.core.devices.led_client import get_provider
|
from ledgrab.core.devices.led_client import get_provider
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
from ledgrab.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from ledgrab.utils.platform import is_android
|
from ledgrab.utils.platform import is_android
|
||||||
@@ -65,7 +65,7 @@ class DiscoveredBLEDevice:
|
|||||||
|
|
||||||
address: str
|
address: str
|
||||||
name: str
|
name: str
|
||||||
rssi: Optional[int]
|
rssi: int | None
|
||||||
service_uuids: tuple = ()
|
service_uuids: tuple = ()
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ class BLETransport:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("BLE disconnect of %s raised: %s", self._address, 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.
|
"""Send bytes to a GATT write characteristic.
|
||||||
|
|
||||||
Serialised through an internal lock — BLE stacks do not like
|
Serialised through an internal lock — BLE stacks do not like
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -48,9 +48,9 @@ class ChromaClient(LEDClient):
|
|||||||
self._base_url = url or CHROMA_SDK_URL
|
self._base_url = url or CHROMA_SDK_URL
|
||||||
self._led_count = led_count
|
self._led_count = led_count
|
||||||
self._chroma_device_type = chroma_device_type
|
self._chroma_device_type = chroma_device_type
|
||||||
self._session_url: Optional[str] = None
|
self._session_url: str | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._heartbeat_task: Optional[asyncio.Task] = None
|
self._heartbeat_task: asyncio.Task | None = None
|
||||||
self._http_client = None
|
self._http_client = None
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
@@ -135,7 +135,7 @@ class ChromaClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.is_connected or not self._http_client:
|
if not self.is_connected or not self._http_client:
|
||||||
@@ -204,7 +204,7 @@ class ChromaClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Check if Chroma SDK is running."""
|
"""Check if Chroma SDK is running."""
|
||||||
base = url or CHROMA_SDK_URL
|
base = url or CHROMA_SDK_URL
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import struct
|
import struct
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -58,12 +58,12 @@ class DDPClient:
|
|||||||
self._sequence = 0
|
self._sequence = 0
|
||||||
self._buses: List[BusConfig] = []
|
self._buses: List[BusConfig] = []
|
||||||
# Pre-allocated RGBW buffer (resized on demand)
|
# 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
|
self._rgbw_buf_n: int = 0
|
||||||
# Pre-allocated send buffer (header + payload). Sized lazily on first
|
# Pre-allocated send buffer (header + payload). Sized lazily on first
|
||||||
# send so we never allocate fresh bytes per frame on the hot path.
|
# send so we never allocate fresh bytes per frame on the hot path.
|
||||||
self._send_buf: Optional[bytearray] = None
|
self._send_buf: bytearray | None = None
|
||||||
self._send_view: Optional[memoryview] = None
|
self._send_view: memoryview | None = None
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Establish UDP connection."""
|
"""Establish UDP connection."""
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -69,7 +69,7 @@ class DDPLEDClient(LEDClient):
|
|||||||
led_count: int = 0,
|
led_count: int = 0,
|
||||||
*,
|
*,
|
||||||
rgbw: bool = False,
|
rgbw: bool = False,
|
||||||
port: Optional[int] = None,
|
port: int | None = None,
|
||||||
destination_id: int = DEFAULT_DESTINATION_ID,
|
destination_id: int = DEFAULT_DESTINATION_ID,
|
||||||
color_order: int = DEFAULT_COLOR_ORDER,
|
color_order: int = DEFAULT_COLOR_ORDER,
|
||||||
):
|
):
|
||||||
@@ -80,7 +80,7 @@ class DDPLEDClient(LEDClient):
|
|||||||
self._rgbw = rgbw
|
self._rgbw = rgbw
|
||||||
self._destination_id = destination_id & 0xFF
|
self._destination_id = destination_id & 0xFF
|
||||||
self._color_order = color_order
|
self._color_order = color_order
|
||||||
self._ddp: Optional[DDPClient] = None
|
self._ddp: DDPClient | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -92,7 +92,7 @@ class DDPLEDClient(LEDClient):
|
|||||||
return self._port
|
return self._port
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
return self._led_count or None
|
return self._led_count or None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -151,7 +151,7 @@ class DDPLEDClient(LEDClient):
|
|||||||
# uint16 scratch avoids overflow; integer divide keeps everything in uint8.
|
# uint16 scratch avoids overflow; integer divide keeps everything in uint8.
|
||||||
return ((pixels.astype(np.uint16) * brightness) // 255).astype(np.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):
|
if isinstance(pixels, np.ndarray):
|
||||||
arr = pixels
|
arr = pixels
|
||||||
else:
|
else:
|
||||||
@@ -164,7 +164,7 @@ class DDPLEDClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
@@ -176,7 +176,7 @@ class DDPLEDClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not self.is_connected or self._ddp is None:
|
if not self.is_connected or self._ddp is None:
|
||||||
@@ -189,7 +189,7 @@ class DDPLEDClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""DDP is connectionless UDP — health = host resolves + port reachable.
|
"""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 dataclasses import dataclass, field
|
||||||
from typing import List, Literal, Optional, Union
|
from typing import List, Literal
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -43,13 +43,13 @@ class DDPConfig(BaseDeviceConfig):
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AdalightConfig(BaseDeviceConfig):
|
class AdalightConfig(BaseDeviceConfig):
|
||||||
device_type: Literal["adalight"] = "adalight"
|
device_type: Literal["adalight"] = "adalight"
|
||||||
baud_rate: Optional[int] = None
|
baud_rate: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AmbiLEDConfig(BaseDeviceConfig):
|
class AmbiLEDConfig(BaseDeviceConfig):
|
||||||
device_type: Literal["ambiled"] = "ambiled"
|
device_type: Literal["ambiled"] = "ambiled"
|
||||||
baud_rate: Optional[int] = None
|
baud_rate: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -63,7 +63,7 @@ class DMXConfig(BaseDeviceConfig):
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ESPNowConfig(BaseDeviceConfig):
|
class ESPNowConfig(BaseDeviceConfig):
|
||||||
device_type: Literal["espnow"] = "espnow"
|
device_type: Literal["espnow"] = "espnow"
|
||||||
baud_rate: Optional[int] = None
|
baud_rate: int | None = None
|
||||||
espnow_peer_mac: str = ""
|
espnow_peer_mac: str = ""
|
||||||
espnow_channel: int = 1
|
espnow_channel: int = 1
|
||||||
|
|
||||||
@@ -217,29 +217,29 @@ class USBHIDConfig(BaseDeviceConfig):
|
|||||||
device_type: Literal["usbhid"] = "usbhid"
|
device_type: Literal["usbhid"] = "usbhid"
|
||||||
|
|
||||||
|
|
||||||
DeviceConfig = Union[
|
DeviceConfig = (
|
||||||
WLEDConfig,
|
WLEDConfig
|
||||||
DDPConfig,
|
| DDPConfig
|
||||||
YeelightConfig,
|
| YeelightConfig
|
||||||
WiZConfig,
|
| WiZConfig
|
||||||
LIFXConfig,
|
| LIFXConfig
|
||||||
GoveeConfig,
|
| GoveeConfig
|
||||||
OPCConfig,
|
| OPCConfig
|
||||||
NanoleafConfig,
|
| NanoleafConfig
|
||||||
AdalightConfig,
|
| AdalightConfig
|
||||||
AmbiLEDConfig,
|
| AmbiLEDConfig
|
||||||
DMXConfig,
|
| DMXConfig
|
||||||
ESPNowConfig,
|
| ESPNowConfig
|
||||||
HueConfig,
|
| HueConfig
|
||||||
SPIConfig,
|
| SPIConfig
|
||||||
ChromaConfig,
|
| ChromaConfig
|
||||||
GameSenseConfig,
|
| GameSenseConfig
|
||||||
BLEConfig,
|
| BLEConfig
|
||||||
GroupConfig,
|
| GroupConfig
|
||||||
MQTTConfig,
|
| MQTTConfig
|
||||||
WSConfig,
|
| WSConfig
|
||||||
USBHIDConfig,
|
| USBHIDConfig
|
||||||
OpenRGBConfig,
|
| OpenRGBConfig
|
||||||
MockConfig,
|
| MockConfig
|
||||||
DemoConfig,
|
| DemoConfig
|
||||||
]
|
)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
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 import ServiceStateChange
|
||||||
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
|
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
|
||||||
@@ -74,9 +74,9 @@ class DiscoveryWatcher:
|
|||||||
self._device_store = device_store
|
self._device_store = device_store
|
||||||
self._fire_event = fire_event
|
self._fire_event = fire_event
|
||||||
|
|
||||||
self._aiozc: Optional[AsyncZeroconf] = None
|
self._aiozc: AsyncZeroconf | None = None
|
||||||
self._browser: Optional[AsyncServiceBrowser] = None
|
self._browser: AsyncServiceBrowser | None = None
|
||||||
self._serial_task: Optional[asyncio.Task] = None
|
self._serial_task: asyncio.Task | None = None
|
||||||
self._running = False
|
self._running = False
|
||||||
self._started_at: float = 0.0
|
self._started_at: float = 0.0
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import struct
|
import struct
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class DMXClient(LEDClient):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
host: str,
|
host: str,
|
||||||
port: Optional[int] = None,
|
port: int | None = None,
|
||||||
led_count: int = 1,
|
led_count: int = 1,
|
||||||
protocol: str = "artnet",
|
protocol: str = "artnet",
|
||||||
start_universe: int = 0,
|
start_universe: int = 0,
|
||||||
@@ -123,7 +123,7 @@ class DMXClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self._transport:
|
if not self._transport:
|
||||||
@@ -133,7 +133,7 @@ class DMXClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not self._transport:
|
if not self._transport:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import struct
|
import struct
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ class ESPNowClient(LEDClient):
|
|||||||
self,
|
self,
|
||||||
url: str = "",
|
url: str = "",
|
||||||
led_count: int = 0,
|
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_peer_mac: str = "FF:FF:FF:FF:FF:FF",
|
||||||
espnow_channel: int = 1,
|
espnow_channel: int = 1,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -109,7 +109,7 @@ class ESPNowClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
@@ -129,7 +129,7 @@ class ESPNowClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
@@ -146,7 +146,7 @@ class ESPNowClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Check if the serial port is available without opening it."""
|
"""Check if the serial port is available without opening it."""
|
||||||
port, _baud = parse_serial_url(url)
|
port, _baud = parse_serial_url(url)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ GAME_DISPLAY_NAME = "LedGrab"
|
|||||||
EVENT_NAME = "PIXEL_DATA"
|
EVENT_NAME = "PIXEL_DATA"
|
||||||
|
|
||||||
|
|
||||||
def _get_gamesense_address() -> Optional[str]:
|
def _get_gamesense_address() -> str | None:
|
||||||
"""Discover the SteelSeries GameSense address from coreProps.json."""
|
"""Discover the SteelSeries GameSense address from coreProps.json."""
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
props_path = os.path.join(
|
props_path = os.path.join(
|
||||||
@@ -77,7 +77,7 @@ class GameSenseClient(LEDClient):
|
|||||||
self._gs_device_type = gamesense_device_type
|
self._gs_device_type = gamesense_device_type
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._http_client = None
|
self._http_client = None
|
||||||
self._base_url: Optional[str] = None
|
self._base_url: str | None = None
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
import httpx
|
import httpx
|
||||||
@@ -174,7 +174,7 @@ class GameSenseClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.is_connected or not self._http_client:
|
if not self.is_connected or not self._http_client:
|
||||||
@@ -227,7 +227,7 @@ class GameSenseClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Check if SteelSeries Engine is running."""
|
"""Check if SteelSeries Engine is running."""
|
||||||
address = url.replace("gamesense://", "").strip() if url else None
|
address = url.replace("gamesense://", "").strip() if url else None
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import json
|
|||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -105,8 +105,8 @@ class GoveeClient(LEDClient):
|
|||||||
self._port = GOVEE_CONTROL_PORT
|
self._port = GOVEE_CONTROL_PORT
|
||||||
self._led_count = led_count
|
self._led_count = led_count
|
||||||
self._min_interval_s = max(0.0, min_interval_s)
|
self._min_interval_s = max(0.0, min_interval_s)
|
||||||
self._transport: Optional[asyncio.DatagramTransport] = None
|
self._transport: asyncio.DatagramTransport | None = None
|
||||||
self._protocol: Optional[_GoveeProtocol] = None
|
self._protocol: _GoveeProtocol | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._next_tx_at: float = 0.0
|
self._next_tx_at: float = 0.0
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ class GoveeClient(LEDClient):
|
|||||||
return self._connected and self._transport is not None
|
return self._connected and self._transport is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
return self._led_count or None
|
return self._led_count or None
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
@@ -185,7 +185,7 @@ class GoveeClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Average the strip → colorwc with the resulting RGB."""
|
"""Average the strip → colorwc with the resulting RGB."""
|
||||||
@@ -206,7 +206,7 @@ class GoveeClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Synchronous variant for the hot loop."""
|
"""Synchronous variant for the hot loop."""
|
||||||
@@ -248,7 +248,7 @@ class GoveeClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Send devStatus and wait briefly for a reply on port 4002.
|
"""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.
|
"""Parse a Govee scan reply into a flat metadata dict.
|
||||||
|
|
||||||
Govee sends ``{"msg": {"cmd": "scan", "data": {"ip": ..., "device": ...,
|
Govee sends ``{"msg": {"cmd": "scan", "data": {"ip": ..., "device": ...,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
from typing import TYPE_CHECKING, List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -82,14 +82,14 @@ class GroupLEDClient(LEDClient):
|
|||||||
return self._connected and all(c.is_connected for c, _ in self._children)
|
return self._connected and all(c.is_connected for c, _ in self._children)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
if self._group_mode == "sequence":
|
if self._group_mode == "sequence":
|
||||||
return self._total_led_count
|
return self._total_led_count
|
||||||
return None # independent mode uses user-specified led_count
|
return None # independent mode uses user-specified led_count
|
||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self._children:
|
if not self._children:
|
||||||
@@ -150,7 +150,7 @@ class GroupLEDClient(LEDClient):
|
|||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
return all(r is True for r in results if not isinstance(r, Exception))
|
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."""
|
"""Snapshot all children's states."""
|
||||||
states = {}
|
states = {}
|
||||||
for i, (client, _) in enumerate(self._children):
|
for i, (client, _) in enumerate(self._children):
|
||||||
@@ -159,7 +159,7 @@ class GroupLEDClient(LEDClient):
|
|||||||
states[i] = state
|
states[i] = state
|
||||||
return states if states else None
|
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."""
|
"""Restore all children's states."""
|
||||||
if not state:
|
if not state:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class HueClient(LEDClient):
|
|||||||
self._username = hue_username
|
self._username = hue_username
|
||||||
self._client_key = hue_client_key
|
self._client_key = hue_client_key
|
||||||
self._group_id = hue_entertainment_group_id
|
self._group_id = hue_entertainment_group_id
|
||||||
self._sock: Optional[socket.socket] = None
|
self._sock: socket.socket | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._sequence = 0
|
self._sequence = 0
|
||||||
self._dtls_sock = None
|
self._dtls_sock = None
|
||||||
@@ -173,7 +173,7 @@ class HueClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
@@ -197,7 +197,7 @@ class HueClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
@@ -210,7 +210,7 @@ class HueClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Check if the Hue bridge is reachable."""
|
"""Check if the Hue bridge is reachable."""
|
||||||
bridge_ip = url.replace("hue://", "").rstrip("/")
|
bridge_ip = url.replace("hue://", "").rstrip("/")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
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
|
import numpy as np
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ class ProviderDeps:
|
|||||||
"""Runtime dependencies injected into every provider.create_client() call."""
|
"""Runtime dependencies injected into every provider.create_client() call."""
|
||||||
|
|
||||||
device_store: Optional["DeviceStore"] = None
|
device_store: Optional["DeviceStore"] = None
|
||||||
mqtt_manager: Optional[object] = None # MQTTManager (avoid circular import)
|
mqtt_manager: object | None = None # MQTTManager (avoid circular import)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -25,16 +25,16 @@ class DeviceHealth:
|
|||||||
"""Health check result for an LED device."""
|
"""Health check result for an LED device."""
|
||||||
|
|
||||||
online: bool = False
|
online: bool = False
|
||||||
latency_ms: Optional[float] = None
|
latency_ms: float | None = None
|
||||||
last_checked: Optional[datetime] = None
|
last_checked: datetime | None = None
|
||||||
# Device-reported metadata (populated by type-specific health check)
|
# Device-reported metadata (populated by type-specific health check)
|
||||||
device_name: Optional[str] = None
|
device_name: str | None = None
|
||||||
device_version: Optional[str] = None
|
device_version: str | None = None
|
||||||
device_led_count: Optional[int] = None
|
device_led_count: int | None = None
|
||||||
device_rgbw: Optional[bool] = None
|
device_rgbw: bool | None = None
|
||||||
device_led_type: Optional[str] = None
|
device_led_type: str | None = None
|
||||||
device_fps: Optional[int] = None
|
device_fps: int | None = None
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PairingNotReady(Exception):
|
class PairingNotReady(Exception):
|
||||||
@@ -55,12 +55,12 @@ class DiscoveredDevice:
|
|||||||
device_type: str
|
device_type: str
|
||||||
ip: str
|
ip: str
|
||||||
mac: str
|
mac: str
|
||||||
led_count: Optional[int]
|
led_count: int | None
|
||||||
version: Optional[str]
|
version: str | None
|
||||||
# Optional provider-specific detected protocol identifier (e.g. BLE family
|
# Optional provider-specific detected protocol identifier (e.g. BLE family
|
||||||
# like "sp110e" / "triones" / "zengge" / "govee"). Surfaced so the UI can
|
# like "sp110e" / "triones" / "zengge" / "govee"). Surfaced so the UI can
|
||||||
# preselect the right sub-type when the user adds a discovered device.
|
# 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):
|
class LEDClient(ABC):
|
||||||
@@ -99,7 +99,7 @@ class LEDClient(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Send pixel colors to the LED device (async).
|
"""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")
|
raise NotImplementedError("send_pixels_fast not supported for this device type")
|
||||||
|
|
||||||
@property
|
@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."""
|
"""Actual LED count discovered after connect(). None if not available."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def snapshot_device_state(self) -> Optional[dict]:
|
async def snapshot_device_state(self) -> dict | None:
|
||||||
"""Snapshot device state before streaming starts.
|
"""Snapshot device state before streaming starts.
|
||||||
|
|
||||||
Override in subclasses that need to save/restore state around streaming.
|
Override in subclasses that need to save/restore state around streaming.
|
||||||
@@ -139,7 +139,7 @@ class LEDClient(ABC):
|
|||||||
"""
|
"""
|
||||||
return None
|
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.
|
"""Restore device state after streaming stops.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -152,7 +152,7 @@ class LEDClient(ABC):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Check device health without a full client connection.
|
"""Check device health without a full client connection.
|
||||||
|
|
||||||
@@ -309,7 +309,7 @@ async def check_device_health(
|
|||||||
device_type: str,
|
device_type: str,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Factory: dispatch health check to the right provider."""
|
"""Factory: dispatch health check to the right provider."""
|
||||||
return await get_provider(device_type).check_health(url, http_client, prev_health)
|
return await get_provider(device_type).check_health(url, http_client, prev_health)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import socket
|
|||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import numpy as np
|
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)
|
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.
|
"""Parse a LIFX StateService (discovery) reply.
|
||||||
|
|
||||||
Returns ``{"mac": "aabbccddeeff", "service": int, "port": int}`` or
|
Returns ``{"mac": "aabbccddeeff", "service": int, "port": int}`` or
|
||||||
@@ -192,8 +192,8 @@ class LIFXClient(LEDClient):
|
|||||||
self._port = port
|
self._port = port
|
||||||
self._led_count = led_count
|
self._led_count = led_count
|
||||||
self._min_interval_s = max(0.0, min_interval_s)
|
self._min_interval_s = max(0.0, min_interval_s)
|
||||||
self._transport: Optional[asyncio.DatagramTransport] = None
|
self._transport: asyncio.DatagramTransport | None = None
|
||||||
self._protocol: Optional[_LIFXProtocol] = None
|
self._protocol: _LIFXProtocol | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._next_tx_at: float = 0.0
|
self._next_tx_at: float = 0.0
|
||||||
self._sequence: int = 0
|
self._sequence: int = 0
|
||||||
@@ -211,7 +211,7 @@ class LIFXClient(LEDClient):
|
|||||||
return self._connected and self._transport is not None
|
return self._connected and self._transport is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
return self._led_count or None
|
return self._led_count or None
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
@@ -257,7 +257,7 @@ class LIFXClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Average the strip → HSBK → SetColor."""
|
"""Average the strip → HSBK → SetColor."""
|
||||||
@@ -279,7 +279,7 @@ class LIFXClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Synchronous variant — same shape, runs on the hot loop."""
|
"""Synchronous variant — same shape, runs on the hot loop."""
|
||||||
@@ -318,7 +318,7 @@ class LIFXClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Send GetService and wait briefly for a StateService reply."""
|
"""Send GetService and wait briefly for a StateService reply."""
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ class MockClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
@@ -64,7 +64,7 @@ class MockClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
return DeviceHealth(
|
return DeviceHealth(
|
||||||
online=True,
|
online=True,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ an ``mqtt_source_id`` so different devices can talk to different brokers.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ class MQTTLEDClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if self._runtime is None:
|
if self._runtime is None:
|
||||||
@@ -111,7 +111,7 @@ class MQTTLEDClient(LEDClient):
|
|||||||
prev_health=None,
|
prev_health=None,
|
||||||
*,
|
*,
|
||||||
mqtt_manager=None,
|
mqtt_manager=None,
|
||||||
mqtt_source_id: Optional[str] = None,
|
mqtt_source_id: str | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -137,7 +137,7 @@ class NanoleafClient(LEDClient):
|
|||||||
self._led_count = led_count
|
self._led_count = led_count
|
||||||
self._min_interval_s = max(0.0, min_interval_s)
|
self._min_interval_s = max(0.0, min_interval_s)
|
||||||
self._request_timeout_s = request_timeout_s
|
self._request_timeout_s = request_timeout_s
|
||||||
self._http: Optional[httpx.AsyncClient] = None
|
self._http: httpx.AsyncClient | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._next_tx_at: float = 0.0
|
self._next_tx_at: float = 0.0
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ class NanoleafClient(LEDClient):
|
|||||||
return self._connected and self._http is not None
|
return self._connected and self._http is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
return self._led_count or None
|
return self._led_count or None
|
||||||
|
|
||||||
def _state_url(self) -> str:
|
def _state_url(self) -> str:
|
||||||
@@ -194,7 +194,7 @@ class NanoleafClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Average the strip and PUT a single HSB state update."""
|
"""Average the strip and PUT a single HSB state update."""
|
||||||
@@ -252,7 +252,7 @@ class NanoleafClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""GET ``/api/v1/<token>/info``. Without a token we can't authenticate,
|
"""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
|
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
|
import asyncio
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -82,8 +82,8 @@ class OPCClient(LEDClient):
|
|||||||
self._led_count = led_count
|
self._led_count = led_count
|
||||||
self._channel = channel & 0xFF
|
self._channel = channel & 0xFF
|
||||||
self._connect_timeout_s = connect_timeout_s
|
self._connect_timeout_s = connect_timeout_s
|
||||||
self._writer: Optional[asyncio.StreamWriter] = None
|
self._writer: asyncio.StreamWriter | None = None
|
||||||
self._reader: Optional[asyncio.StreamReader] = None
|
self._reader: asyncio.StreamReader | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -103,7 +103,7 @@ class OPCClient(LEDClient):
|
|||||||
return self._connected and self._writer is not None
|
return self._connected and self._writer is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
return self._led_count or None
|
return self._led_count or None
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
@@ -144,7 +144,7 @@ class OPCClient(LEDClient):
|
|||||||
return np.zeros_like(pixels)
|
return np.zeros_like(pixels)
|
||||||
return ((pixels.astype(np.uint16) * brightness) // 255).astype(np.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):
|
if isinstance(pixels, np.ndarray):
|
||||||
arr = pixels
|
arr = pixels
|
||||||
else:
|
else:
|
||||||
@@ -159,7 +159,7 @@ class OPCClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
@@ -175,7 +175,7 @@ class OPCClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Synchronous hot-path write. Drain runs implicitly when the OS buffer
|
"""Synchronous hot-path write. Drain runs implicitly when the OS buffer
|
||||||
@@ -197,7 +197,7 @@ class OPCClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Open a TCP connection and close it. OPC has no protocol-level
|
"""Open a TCP connection and close it. OPC has no protocol-level
|
||||||
ping; reachable TCP is the strongest signal we get."""
|
ping; reachable TCP is the strongest signal we get."""
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import socket
|
|||||||
import struct
|
import struct
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, List, Optional, Tuple, Union
|
from typing import Any, List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
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.
|
When *zone_names* is non-empty, only LEDs in those zones are addressed.
|
||||||
Multiple zones are separated by ``+``.
|
Multiple zones are separated by ``+``.
|
||||||
"""
|
"""
|
||||||
zones_str: Optional[str] = None
|
zones_str: str | None = None
|
||||||
|
|
||||||
if url.startswith("openrgb://"):
|
if url.startswith("openrgb://"):
|
||||||
url = url[len("openrgb://") :]
|
url = url[len("openrgb://") :]
|
||||||
@@ -87,16 +87,16 @@ class OpenRGBLEDClient(LEDClient):
|
|||||||
self._client: Any = None # openrgb.OpenRGBClient
|
self._client: Any = None # openrgb.OpenRGBClient
|
||||||
self._device: Any = None # openrgb.Device
|
self._device: Any = None # openrgb.Device
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._device_name: Optional[str] = None
|
self._device_name: str | None = None
|
||||||
self._device_led_count: Optional[int] = None
|
self._device_led_count: int | None = None
|
||||||
|
|
||||||
# Background sender thread — decouples processing loop from blocking TCP writes
|
# Background sender thread — decouples processing loop from blocking TCP writes
|
||||||
self._send_lock = threading.Lock()
|
self._send_lock = threading.Lock()
|
||||||
self._send_event = threading.Event()
|
self._send_event = threading.Event()
|
||||||
self._send_pending: Optional[Tuple[np.ndarray, int]] = None # (pixels, brightness)
|
self._send_pending: Tuple[np.ndarray, int] | None = None # (pixels, brightness)
|
||||||
self._send_thread: Optional[threading.Thread] = None
|
self._send_thread: threading.Thread | None = None
|
||||||
self._send_stop = threading.Event()
|
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:
|
async def connect(self) -> bool:
|
||||||
"""Connect to OpenRGB server and access the target device."""
|
"""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
|
return self._connected and self._client is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
return self._device_led_count
|
return self._device_led_count
|
||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Send pixel colors to the OpenRGB device (async wrapper)."""
|
"""Send pixel colors to the OpenRGB device (async wrapper)."""
|
||||||
@@ -232,7 +232,7 @@ class OpenRGBLEDClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Non-blocking fire-and-forget send for the processing hot loop.
|
"""Non-blocking fire-and-forget send for the processing hot loop.
|
||||||
@@ -377,7 +377,7 @@ class OpenRGBLEDClient(LEDClient):
|
|||||||
finally:
|
finally:
|
||||||
comms.lock.release()
|
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."""
|
"""Save the active mode index before streaming."""
|
||||||
if self._device is None:
|
if self._device is None:
|
||||||
return None
|
return None
|
||||||
@@ -387,7 +387,7 @@ class OpenRGBLEDClient(LEDClient):
|
|||||||
logger.warning(f"Could not snapshot OpenRGB device state: {e}")
|
logger.warning(f"Could not snapshot OpenRGB device state: {e}")
|
||||||
return None
|
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."""
|
"""Restore the original mode after streaming stops."""
|
||||||
if not state or self._device is None:
|
if not state or self._device is None:
|
||||||
return
|
return
|
||||||
@@ -404,7 +404,7 @@ class OpenRGBLEDClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Check OpenRGB server reachability via raw TCP socket connect.
|
"""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 __future__ import annotations
|
||||||
|
|
||||||
from typing import List, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
def average_color(
|
def average_color(
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
) -> Tuple[int, int, int]:
|
) -> Tuple[int, int, int]:
|
||||||
"""Reduce an N-pixel strip to one average RGB triple.
|
"""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 __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional, Protocol
|
from typing import List, Protocol
|
||||||
|
|
||||||
from ledgrab.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
from ledgrab.utils.platform import is_android
|
from ledgrab.utils.platform import is_android
|
||||||
@@ -119,7 +119,7 @@ def port_exists(device: str) -> bool:
|
|||||||
|
|
||||||
def open_transport(
|
def open_transport(
|
||||||
url: str,
|
url: str,
|
||||||
baud_rate: Optional[int] = None,
|
baud_rate: int | None = None,
|
||||||
timeout: float = 1.0,
|
timeout: float = 1.0,
|
||||||
) -> SerialTransport:
|
) -> SerialTransport:
|
||||||
"""Construct an unopened transport for ``url``. Caller invokes ``open()``."""
|
"""Construct an unopened transport for ``url``. Caller invokes ``open()``."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class SPIClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
@@ -205,7 +205,7 @@ class SPIClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
@@ -222,7 +222,7 @@ class SPIClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Check if the SPI/GPIO device is accessible."""
|
"""Check if the SPI/GPIO device is accessible."""
|
||||||
import platform
|
import platform
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ class USBHIDClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
@@ -144,7 +144,7 @@ class USBHIDClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Check if the HID device is present."""
|
"""Check if the HID device is present."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import json
|
|||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -82,8 +82,8 @@ class WiZClient(LEDClient):
|
|||||||
self._port = port
|
self._port = port
|
||||||
self._led_count = led_count
|
self._led_count = led_count
|
||||||
self._min_interval_s = max(0.0, min_interval_s)
|
self._min_interval_s = max(0.0, min_interval_s)
|
||||||
self._transport: Optional[asyncio.DatagramTransport] = None
|
self._transport: asyncio.DatagramTransport | None = None
|
||||||
self._protocol: Optional[_WiZProtocol] = None
|
self._protocol: _WiZProtocol | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._next_tx_at: float = 0.0
|
self._next_tx_at: float = 0.0
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class WiZClient(LEDClient):
|
|||||||
return self._connected and self._transport is not None
|
return self._connected and self._transport is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
return self._led_count or None
|
return self._led_count or None
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
@@ -137,7 +137,7 @@ class WiZClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Average the pixel strip to one color and push ``setPilot``."""
|
"""Average the pixel strip to one color and push ``setPilot``."""
|
||||||
@@ -161,7 +161,7 @@ class WiZClient(LEDClient):
|
|||||||
|
|
||||||
def send_pixels_fast(
|
def send_pixels_fast(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Synchronous variant for the hot path. Same shape as send_pixels."""
|
"""Synchronous variant for the hot path. Same shape as send_pixels."""
|
||||||
@@ -205,7 +205,7 @@ class WiZClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Send a getPilot and wait briefly for any reply on a one-shot socket."""
|
"""Send a getPilot and wait briefly for any reply on a one-shot socket."""
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timezone
|
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
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -106,10 +106,10 @@ class WLEDClient(LEDClient):
|
|||||||
parsed = urlparse(self.url)
|
parsed = urlparse(self.url)
|
||||||
self.host = parsed.hostname or parsed.netloc.split(":")[0]
|
self.host = parsed.hostname or parsed.netloc.split(":")[0]
|
||||||
|
|
||||||
self._client: Optional[httpx.AsyncClient] = None
|
self._client: httpx.AsyncClient | None = None
|
||||||
self._ddp_client: Optional[DDPClient] = None
|
self._ddp_client: DDPClient | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._pre_connect_state: Optional[dict] = None
|
self._pre_connect_state: dict | None = None
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
"""Establish connection to WLED device.
|
"""Establish connection to WLED device.
|
||||||
@@ -208,7 +208,7 @@ class WLEDClient(LEDClient):
|
|||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
endpoint: str,
|
endpoint: str,
|
||||||
json_data: Optional[Dict[str, Any]] = None,
|
json_data: Dict[str, Any] | None = None,
|
||||||
retry: bool = True,
|
retry: bool = True,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Make HTTP request to WLED device with retry logic.
|
"""Make HTTP request to WLED device with retry logic.
|
||||||
@@ -498,7 +498,7 @@ class WLEDClient(LEDClient):
|
|||||||
|
|
||||||
# ===== LEDClient abstraction methods =====
|
# ===== 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).
|
"""Snapshot WLED state (on, lor, AudioReactive).
|
||||||
|
|
||||||
If connect() already captured a pre-mutation snapshot, returns that
|
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}")
|
logger.warning(f"Could not snapshot WLED state: {e}")
|
||||||
return None
|
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."""
|
"""Restore WLED state after streaming."""
|
||||||
if not state:
|
if not state:
|
||||||
return
|
return
|
||||||
@@ -541,7 +541,7 @@ class WLEDClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""WLED health check via GET /json/info (+ /json/cfg for LED type)."""
|
"""WLED health check via GET /json/info (+ /json/cfg for LED type)."""
|
||||||
url = url.rstrip("/")
|
url = url.rstrip("/")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
from typing import TYPE_CHECKING, Dict, List, Tuple
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from zeroconf import ServiceStateChange
|
from zeroconf import ServiceStateChange
|
||||||
@@ -47,7 +47,7 @@ class WLEDDeviceProvider(LEDDeviceProvider):
|
|||||||
"""Provider for WLED LED controllers."""
|
"""Provider for WLED LED controllers."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._http_client: Optional[httpx.AsyncClient] = None
|
self._http_client: httpx.AsyncClient | None = None
|
||||||
self._client_lock = asyncio.Lock()
|
self._client_lock = asyncio.Lock()
|
||||||
# Per-base-URL state cache: base -> (expires_at, json_state_dict)
|
# Per-base-URL state cache: base -> (expires_at, json_state_dict)
|
||||||
self._state_cache: Dict[str, tuple] = {}
|
self._state_cache: Dict[str, tuple] = {}
|
||||||
@@ -186,7 +186,7 @@ class WLEDDeviceProvider(LEDDeviceProvider):
|
|||||||
|
|
||||||
async def _enrich_device(
|
async def _enrich_device(
|
||||||
self, url: str, fallback_name: str
|
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.
|
"""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.
|
Reuses the shared HTTP client so discovery probes share connection-pool state.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class WSLEDClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
@@ -123,7 +123,7 @@ class WSLEDClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
return DeviceHealth(
|
return DeviceHealth(
|
||||||
online=True,
|
online=True,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import json
|
|||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Optional, Tuple, Union
|
from typing import List, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -77,8 +77,8 @@ class YeelightClient(LEDClient):
|
|||||||
self._led_count = led_count
|
self._led_count = led_count
|
||||||
self._min_interval_s = max(0.0, min_interval_s)
|
self._min_interval_s = max(0.0, min_interval_s)
|
||||||
self._connect_timeout_s = connect_timeout_s
|
self._connect_timeout_s = connect_timeout_s
|
||||||
self._reader: Optional[asyncio.StreamReader] = None
|
self._reader: asyncio.StreamReader | None = None
|
||||||
self._writer: Optional[asyncio.StreamWriter] = None
|
self._writer: asyncio.StreamWriter | None = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._next_tx_at: float = 0.0
|
self._next_tx_at: float = 0.0
|
||||||
self._req_id: int = 0
|
self._req_id: int = 0
|
||||||
@@ -93,7 +93,7 @@ class YeelightClient(LEDClient):
|
|||||||
return self._connected and self._writer is not None
|
return self._connected and self._writer is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_led_count(self) -> Optional[int]:
|
def device_led_count(self) -> int | None:
|
||||||
return self._led_count or None
|
return self._led_count or None
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
@@ -141,7 +141,7 @@ class YeelightClient(LEDClient):
|
|||||||
|
|
||||||
async def send_pixels(
|
async def send_pixels(
|
||||||
self,
|
self,
|
||||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||||
brightness: int = 255,
|
brightness: int = 255,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Average the pixel strip to one color and ``set_rgb``.
|
"""Average the pixel strip to one color and ``set_rgb``.
|
||||||
@@ -185,7 +185,7 @@ class YeelightClient(LEDClient):
|
|||||||
cls,
|
cls,
|
||||||
url: str,
|
url: str,
|
||||||
http_client,
|
http_client,
|
||||||
prev_health: Optional[DeviceHealth] = None,
|
prev_health: DeviceHealth | None = None,
|
||||||
) -> DeviceHealth:
|
) -> DeviceHealth:
|
||||||
"""Health check: open the TCP socket to the bulb and close it."""
|
"""Health check: open the TCP socket to the bulb and close it."""
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@@ -230,7 +230,7 @@ _DISCOVER_REQUEST = (
|
|||||||
).encode("ascii")
|
).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.
|
"""Parse a Yeelight discovery response into a ``{header: value}`` dict.
|
||||||
|
|
||||||
Returns ``None`` when the payload doesn't look like a Yeelight reply
|
Returns ``None`` when the payload doesn't look like a Yeelight reply
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Auto-crop postprocessing filter."""
|
"""Auto-crop postprocessing filter."""
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
import numpy as np
|
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)
|
threshold = self.options.get("threshold", 15)
|
||||||
min_bar_size = self.options.get("min_bar_size", 20)
|
min_bar_size = self.options.get("min_bar_size", 20)
|
||||||
min_aspect_ratio = float(self.options.get("min_aspect_ratio", 0.0))
|
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 abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
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
|
import numpy as np
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ class FilterOptionDef:
|
|||||||
min_value: Any
|
min_value: Any
|
||||||
max_value: Any
|
max_value: Any
|
||||||
step: Any
|
step: Any
|
||||||
choices: Optional[List[Dict[str, str]]] = None # for "select": [{value, label}]
|
choices: List[Dict[str, str]] | None = None # for "select": [{value, label}]
|
||||||
max_length: Optional[int] = None # for "string" type
|
max_length: int | None = None # for "string" type
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
d = {
|
d = {
|
||||||
@@ -76,7 +76,7 @@ class PostprocessingFilter(ABC):
|
|||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@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.
|
"""Process image.
|
||||||
|
|
||||||
Args:
|
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.
|
"""Process a 1D LED strip array (N, 3) uint8.
|
||||||
|
|
||||||
Default implementation reshapes to (1, N, 3), calls process_image
|
Default implementation reshapes to (1, N, 3), calls process_image
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Brightness postprocessing filter."""
|
"""Brightness postprocessing filter."""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
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:
|
if self.options["value"] == 1.0:
|
||||||
return None
|
return None
|
||||||
image[:] = self._lut[image]
|
image[:] = self._lut[image]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Color correction postprocessing filter."""
|
"""Color correction postprocessing filter."""
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
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:
|
if self._is_neutral:
|
||||||
return None
|
return None
|
||||||
image[:] = self._lut[image]
|
image[:] = self._lut[image]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Contrast postprocessing filter."""
|
"""Contrast postprocessing filter."""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
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:
|
if self.options["value"] == 1.0:
|
||||||
return None
|
return None
|
||||||
image[:] = self._lut[image]
|
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.
|
template's filters when building the processing pipeline.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
import numpy as np
|
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.
|
# Never called — expanded at pipeline build time.
|
||||||
return None
|
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.
|
# Never called — expanded at pipeline build time.
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Downscaler postprocessing filter."""
|
"""Downscaler postprocessing filter."""
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
import numpy as np
|
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"]
|
factor = self.options["factor"]
|
||||||
if factor >= 1.0:
|
if factor >= 1.0:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ instantiated at runtime: ``LiveStreamManager`` expands it into the
|
|||||||
referenced template's filters when building the processing pipeline.
|
referenced template's filters when building the processing pipeline.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
import numpy as np
|
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.
|
# Never called — expanded at pipeline build time by LiveStreamManager.
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Flip postprocessing filter."""
|
"""Flip postprocessing filter."""
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
import numpy as np
|
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)
|
h = self.options.get("horizontal", False)
|
||||||
v = self.options.get("vertical", False)
|
v = self.options.get("vertical", False)
|
||||||
if not h and not v:
|
if not h and not v:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Frame interpolation postprocessing filter."""
|
"""Frame interpolation postprocessing filter."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -38,21 +38,21 @@ class FrameInterpolationFilter(PostprocessingFilter):
|
|||||||
|
|
||||||
def __init__(self, options: Dict[str, Any]):
|
def __init__(self, options: Dict[str, Any]):
|
||||||
super().__init__(options)
|
super().__init__(options)
|
||||||
self._frame_a: Optional[np.ndarray] = None # frame N-1
|
self._frame_a: np.ndarray | None = None # frame N-1
|
||||||
self._frame_b: Optional[np.ndarray] = None # frame N (latest source)
|
self._frame_b: np.ndarray | None = None # frame N (latest source)
|
||||||
self._time_a: float = 0.0
|
self._time_a: float = 0.0
|
||||||
self._time_b: 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
|
# Pre-allocated uint16 scratch buffers for blending
|
||||||
self._u16_a: Optional[np.ndarray] = None
|
self._u16_a: np.ndarray | None = None
|
||||||
self._u16_b: Optional[np.ndarray] = None
|
self._u16_b: np.ndarray | None = None
|
||||||
self._blend_shape: Optional[tuple] = None
|
self._blend_shape: tuple | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_options_schema(cls) -> List[FilterOptionDef]:
|
def get_options_schema(cls) -> List[FilterOptionDef]:
|
||||||
return []
|
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.
|
"""Return interpolated blend on idle ticks; update state on new source frames.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -61,11 +61,11 @@ class FrameInterpolationFilter(PostprocessingFilter):
|
|||||||
"""
|
"""
|
||||||
return self._blend(image, lambda shape: image_pool.acquire(*shape))
|
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."""
|
"""Frame interpolation for 1D LED strips — allocates directly."""
|
||||||
return self._blend(strip, lambda shape: np.empty(shape, dtype=np.uint8))
|
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."""
|
"""Shared blend logic for both images and strips."""
|
||||||
now = time.perf_counter()
|
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