refactor: rename project to LedGrab, split HA integration into separate repo
Lint & Test / test (push) Successful in 1m56s
Lint & Test / test (push) Successful in 1m56s
- Rename Python package: wled_controller -> ledgrab - Rename env var prefix: WLED_ -> LEDGRAB_ (with auto-migration for old vars) - Rename localStorage key: wled_api_key -> ledgrab_api_key (with migration) - Rename HA integration domain: wled_screen_controller -> ledgrab - Update all imports, build scripts, Docker, installer, config, docs - Remove HA integration (moved to ledgrab-haos-integration repo) - Remove hacs.json (belongs in HA repo now) - Add startup warning for users with old WLED_ env vars - All tests pass (715/715), ruff clean, tsc clean, frontend builds
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
from importlib.metadata import version, PackageNotFoundError
|
||||
|
||||
try:
|
||||
__version__ = version("wled-screen-controller")
|
||||
__version__ = version("ledgrab")
|
||||
except PackageNotFoundError:
|
||||
# Running from source without pip install (e.g. dev, embedded Python)
|
||||
__version__ = "0.0.0-dev"
|
||||
@@ -13,6 +13,6 @@ __email__ = "dolgolyov.alexei@gmail.com"
|
||||
|
||||
# ─── Project links ───────────────────────────────────────────
|
||||
GITEA_BASE_URL = "https://git.dolgolyov-family.by"
|
||||
GITEA_REPO = "alexei.dolgolyov/wled-screen-controller-mixed"
|
||||
GITEA_REPO = "alexei.dolgolyov/ledgrab"
|
||||
REPO_URL = f"{GITEA_BASE_URL}/{GITEA_REPO}"
|
||||
DONATE_URL = "" # TODO: set once donation platform is chosen
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Entry point for ``python -m wled_controller``.
|
||||
"""Entry point for ``python -m ledgrab``.
|
||||
|
||||
Starts the uvicorn server and, on Windows when *pystray* is installed,
|
||||
shows a system-tray icon with **Show UI** / **Exit** actions.
|
||||
@@ -36,10 +36,10 @@ _fix_embedded_tcl_paths()
|
||||
|
||||
import uvicorn # noqa: E402
|
||||
|
||||
from wled_controller.config import get_config # noqa: E402
|
||||
from wled_controller.server_ref import set_server, set_tray # noqa: E402
|
||||
from wled_controller.tray import PYSTRAY_AVAILABLE, TrayManager # noqa: E402
|
||||
from wled_controller.utils import setup_logging, get_logger # noqa: E402
|
||||
from ledgrab.config import get_config # noqa: E402
|
||||
from ledgrab.server_ref import set_server, set_tray # noqa: E402
|
||||
from ledgrab.tray import PYSTRAY_AVAILABLE, TrayManager # noqa: E402
|
||||
from ledgrab.utils import setup_logging, get_logger # noqa: E402
|
||||
|
||||
setup_logging()
|
||||
logger = get_logger(__name__)
|
||||
@@ -62,7 +62,7 @@ def _open_browser(port: int, delay: float = 2.0) -> None:
|
||||
|
||||
def _is_restart() -> bool:
|
||||
"""Detect if this is a restart (vs first launch)."""
|
||||
return os.environ.get("WLED_RESTART", "") == "1"
|
||||
return os.environ.get("LEDGRAB_RESTART", "") == "1"
|
||||
|
||||
|
||||
def _check_port(host: str, port: int) -> None:
|
||||
@@ -81,7 +81,7 @@ def main() -> None:
|
||||
_check_port(config.server.host, config.server.port)
|
||||
|
||||
uv_config = uvicorn.Config(
|
||||
"wled_controller.main:app",
|
||||
"ledgrab.main:app",
|
||||
host=config.server.host,
|
||||
port=config.server.port,
|
||||
log_level=config.server.log_level.lower(),
|
||||
@@ -133,10 +133,10 @@ def _request_shutdown(server: uvicorn.Server) -> None:
|
||||
|
||||
|
||||
def _force_tray() -> bool:
|
||||
"""Allow forcing tray on non-Windows via WLED_TRAY=1."""
|
||||
"""Allow forcing tray on non-Windows via LEDGRAB_TRAY=1."""
|
||||
import os
|
||||
|
||||
return os.environ.get("WLED_TRAY", "").strip() in ("1", "true", "yes")
|
||||
return os.environ.get("LEDGRAB_TRAY", "").strip() in ("1", "true", "yes")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -6,8 +6,8 @@ from typing import Annotated
|
||||
from fastapi import Depends, HTTPException, Security, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from wled_controller.config import get_config
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.config import get_config
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
+31
-31
@@ -6,39 +6,39 @@ All getter function signatures remain unchanged for FastAPI Depends() compatibil
|
||||
|
||||
from typing import Any, Dict, TypeVar
|
||||
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.storage.database import Database
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.storage.template_store import TemplateStore
|
||||
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.audio_source_store import AudioSourceStore
|
||||
from wled_controller.storage.audio_template_store import AudioTemplateStore
|
||||
from wled_controller.storage.value_source_store import ValueSourceStore
|
||||
from wled_controller.storage.automation_store import AutomationStore
|
||||
from wled_controller.storage.scene_preset_store import ScenePresetStore
|
||||
from wled_controller.storage.sync_clock_store import SyncClockStore
|
||||
from wled_controller.storage.color_strip_processing_template_store import (
|
||||
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||
from ledgrab.storage.database import Database
|
||||
from ledgrab.storage import DeviceStore
|
||||
from ledgrab.storage.template_store import TemplateStore
|
||||
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||
from ledgrab.storage.audio_source_store import AudioSourceStore
|
||||
from ledgrab.storage.audio_template_store import AudioTemplateStore
|
||||
from ledgrab.storage.value_source_store import ValueSourceStore
|
||||
from ledgrab.storage.automation_store import AutomationStore
|
||||
from ledgrab.storage.scene_preset_store import ScenePresetStore
|
||||
from ledgrab.storage.sync_clock_store import SyncClockStore
|
||||
from ledgrab.storage.color_strip_processing_template_store import (
|
||||
ColorStripProcessingTemplateStore,
|
||||
)
|
||||
from wled_controller.storage.gradient_store import GradientStore
|
||||
from wled_controller.storage.weather_source_store import WeatherSourceStore
|
||||
from wled_controller.storage.asset_store import AssetStore
|
||||
from wled_controller.core.automations.automation_engine import AutomationEngine
|
||||
from wled_controller.core.weather.weather_manager import WeatherManager
|
||||
from wled_controller.core.backup.auto_backup import AutoBackupEngine
|
||||
from wled_controller.core.processing.sync_clock_manager import SyncClockManager
|
||||
from wled_controller.core.update.update_service import UpdateService
|
||||
from wled_controller.storage.home_assistant_store import HomeAssistantStore
|
||||
from wled_controller.core.home_assistant.ha_manager import HomeAssistantManager
|
||||
from wled_controller.storage.game_integration_store import GameIntegrationStore
|
||||
from wled_controller.core.game_integration.event_bus import GameEventBus
|
||||
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
||||
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
from wled_controller.storage.pattern_template_store import PatternTemplateStore
|
||||
from ledgrab.storage.gradient_store import GradientStore
|
||||
from ledgrab.storage.weather_source_store import WeatherSourceStore
|
||||
from ledgrab.storage.asset_store import AssetStore
|
||||
from ledgrab.core.automations.automation_engine import AutomationEngine
|
||||
from ledgrab.core.weather.weather_manager import WeatherManager
|
||||
from ledgrab.core.backup.auto_backup import AutoBackupEngine
|
||||
from ledgrab.core.processing.sync_clock_manager import SyncClockManager
|
||||
from ledgrab.core.update.update_service import UpdateService
|
||||
from ledgrab.storage.home_assistant_store import HomeAssistantStore
|
||||
from ledgrab.core.home_assistant.ha_manager import HomeAssistantManager
|
||||
from ledgrab.storage.game_integration_store import GameIntegrationStore
|
||||
from ledgrab.core.game_integration.event_bus import GameEventBus
|
||||
from ledgrab.storage.mqtt_source_store import MQTTSourceStore
|
||||
from ledgrab.core.mqtt.mqtt_manager import MQTTManager
|
||||
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
from ledgrab.storage.pattern_template_store import PatternTemplateStore
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
+28
-23
@@ -8,9 +8,9 @@ from typing import Callable, Optional
|
||||
import numpy as np
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
from wled_controller.core.filters import FilterRegistry, ImagePool
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.utils.image_codec import (
|
||||
from ledgrab.core.filters import FilterRegistry, ImagePool
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.utils.image_codec import (
|
||||
encode_jpeg,
|
||||
encode_jpeg_data_uri,
|
||||
resize_down,
|
||||
@@ -31,7 +31,8 @@ def authenticate_ws_token(token: str) -> bool:
|
||||
|
||||
Delegates to the canonical implementation in auth module.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
return verify_ws_token(token)
|
||||
|
||||
|
||||
@@ -160,14 +161,16 @@ async def stream_capture_test(
|
||||
thumb_uri = _encode_jpeg(thumb, PREVIEW_JPEG_QUALITY)
|
||||
fps = fc / elapsed if elapsed > 0 else 0
|
||||
avg_ms = (tc / fc * 1000) if fc > 0 else 0
|
||||
await websocket.send_json({
|
||||
"type": "frame",
|
||||
"thumbnail": thumb_uri,
|
||||
"frame_count": fc,
|
||||
"elapsed_s": round(elapsed, 2),
|
||||
"fps": round(fps, 1),
|
||||
"avg_capture_ms": round(avg_ms, 1),
|
||||
})
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "frame",
|
||||
"thumbnail": thumb_uri,
|
||||
"frame_count": fc,
|
||||
"elapsed_s": round(elapsed, 2),
|
||||
"fps": round(fps, 1),
|
||||
"avg_capture_ms": round(avg_ms, 1),
|
||||
}
|
||||
)
|
||||
|
||||
# Wait for capture thread to fully finish
|
||||
await capture_future
|
||||
@@ -199,17 +202,19 @@ async def stream_capture_test(
|
||||
thumb = _make_thumbnail(final_frame, FINAL_THUMBNAIL_WIDTH)
|
||||
thumb_uri = _encode_jpeg(thumb, 85)
|
||||
|
||||
await websocket.send_json({
|
||||
"type": "result",
|
||||
"full_image": full_uri,
|
||||
"thumbnail": thumb_uri,
|
||||
"width": w,
|
||||
"height": h,
|
||||
"frame_count": fc,
|
||||
"elapsed_s": round(elapsed, 2),
|
||||
"fps": round(fps, 1),
|
||||
"avg_capture_ms": round(avg_ms, 1),
|
||||
})
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "result",
|
||||
"full_image": full_uri,
|
||||
"thumbnail": thumb_uri,
|
||||
"width": w,
|
||||
"height": h,
|
||||
"frame_count": fc,
|
||||
"elapsed_s": round(elapsed, 2),
|
||||
"fps": round(fps, 1),
|
||||
"avg_capture_ms": round(avg_ms, 1),
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# WebSocket disconnect or send error — signal capture thread to stop
|
||||
+10
-8
@@ -5,17 +5,17 @@ from pathlib import Path
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import fire_entity_event, get_asset_store
|
||||
from wled_controller.api.schemas.assets import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import fire_entity_event, get_asset_store
|
||||
from ledgrab.api.schemas.assets import (
|
||||
AssetListResponse,
|
||||
AssetResponse,
|
||||
AssetUpdate,
|
||||
)
|
||||
from wled_controller.config import get_config
|
||||
from wled_controller.storage.asset_store import AssetStore
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.config import get_config
|
||||
from ledgrab.storage.asset_store import AssetStore
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -103,7 +103,9 @@ async def upload_asset(
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail="Empty file")
|
||||
|
||||
display_name = name or Path(file.filename or "unnamed").stem.replace("_", " ").replace("-", " ").title()
|
||||
display_name = (
|
||||
name or Path(file.filename or "unnamed").stem.replace("_", " ").replace("-", " ").title()
|
||||
)
|
||||
|
||||
try:
|
||||
asset = store.create_asset(
|
||||
+2
-2
@@ -4,8 +4,8 @@ import asyncio
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.core.audio.audio_capture import AudioCaptureManager
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.core.audio.audio_capture import AudioCaptureManager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
+5
-5
@@ -2,15 +2,15 @@
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import get_audio_processing_template_store
|
||||
from wled_controller.api.schemas.filters import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import get_audio_processing_template_store
|
||||
from ledgrab.api.schemas.filters import (
|
||||
FilterOptionDefSchema,
|
||||
FilterTypeListResponse,
|
||||
FilterTypeResponse,
|
||||
)
|
||||
from wled_controller.core.audio.filters import AudioFilterRegistry
|
||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
from ledgrab.core.audio.filters import AudioFilterRegistry
|
||||
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
+8
-8
@@ -2,24 +2,24 @@
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_audio_processing_template_store,
|
||||
get_audio_source_store,
|
||||
get_processor_manager,
|
||||
)
|
||||
from wled_controller.api.schemas.audio_processing import (
|
||||
from ledgrab.api.schemas.audio_processing import (
|
||||
AudioProcessingTemplateCreate,
|
||||
AudioProcessingTemplateListResponse,
|
||||
AudioProcessingTemplateResponse,
|
||||
AudioProcessingTemplateUpdate,
|
||||
)
|
||||
from wled_controller.api.schemas.filters import FilterInstanceSchema
|
||||
from wled_controller.core.filters.filter_instance import FilterInstance
|
||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.api.schemas.filters import FilterInstanceSchema
|
||||
from ledgrab.core.filters.filter_instance import FilterInstance
|
||||
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
+11
-11
@@ -6,8 +6,8 @@ from typing import Annotated, Optional
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_audio_processing_template_store,
|
||||
get_audio_source_store,
|
||||
@@ -15,7 +15,7 @@ from wled_controller.api.dependencies import (
|
||||
get_color_strip_store,
|
||||
get_processor_manager,
|
||||
)
|
||||
from wled_controller.api.schemas.audio_sources import (
|
||||
from ledgrab.api.schemas.audio_sources import (
|
||||
AudioSourceCreate,
|
||||
AudioSourceListResponse,
|
||||
AudioSourceResponse,
|
||||
@@ -23,15 +23,15 @@ from wled_controller.api.schemas.audio_sources import (
|
||||
CaptureAudioSourceResponse,
|
||||
ProcessedAudioSourceResponse,
|
||||
)
|
||||
from wled_controller.storage.audio_source import (
|
||||
from ledgrab.storage.audio_source import (
|
||||
AudioSource,
|
||||
CaptureAudioSource,
|
||||
ProcessedAudioSource,
|
||||
)
|
||||
from wled_controller.storage.audio_source_store import AudioSourceStore
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.audio_source_store import AudioSourceStore
|
||||
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -178,7 +178,7 @@ async def delete_audio_source(
|
||||
"""Delete an audio source."""
|
||||
try:
|
||||
# Check if any CSS entities reference this audio source
|
||||
from wled_controller.storage.color_strip_source import AudioColorStripSource
|
||||
from ledgrab.storage.color_strip_source import AudioColorStripSource
|
||||
|
||||
for css in css_store.get_all_sources():
|
||||
if (
|
||||
@@ -215,8 +215,8 @@ async def test_audio_source_ws(
|
||||
analysis before sending, so the WebSocket output matches what running
|
||||
streams see.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from wled_controller.core.audio.filters.pipeline import build_pipeline_from_template_ids
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
from ledgrab.core.audio.filters.pipeline import build_pipeline_from_template_ids
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
+92
-40
@@ -4,9 +4,14 @@ import asyncio
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import fire_entity_event, get_audio_template_store, get_audio_source_store, get_processor_manager
|
||||
from wled_controller.api.schemas.audio_templates import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_audio_template_store,
|
||||
get_audio_source_store,
|
||||
get_processor_manager,
|
||||
)
|
||||
from ledgrab.api.schemas.audio_templates import (
|
||||
AudioEngineInfo,
|
||||
AudioEngineListResponse,
|
||||
AudioTemplateCreate,
|
||||
@@ -14,11 +19,11 @@ from wled_controller.api.schemas.audio_templates import (
|
||||
AudioTemplateResponse,
|
||||
AudioTemplateUpdate,
|
||||
)
|
||||
from wled_controller.core.audio.factory import AudioEngineRegistry
|
||||
from wled_controller.storage.audio_template_store import AudioTemplateStore
|
||||
from wled_controller.storage.audio_source_store import AudioSourceStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.core.audio.factory import AudioEngineRegistry
|
||||
from ledgrab.storage.audio_template_store import AudioTemplateStore
|
||||
from ledgrab.storage.audio_source_store import AudioSourceStore
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -27,7 +32,10 @@ router = APIRouter()
|
||||
|
||||
# ===== AUDIO TEMPLATE ENDPOINTS =====
|
||||
|
||||
@router.get("/api/v1/audio-templates", response_model=AudioTemplateListResponse, tags=["Audio Templates"])
|
||||
|
||||
@router.get(
|
||||
"/api/v1/audio-templates", response_model=AudioTemplateListResponse, tags=["Audio Templates"]
|
||||
)
|
||||
async def list_audio_templates(
|
||||
_auth: AuthRequired,
|
||||
store: AudioTemplateStore = Depends(get_audio_template_store),
|
||||
@@ -37,10 +45,14 @@ async def list_audio_templates(
|
||||
templates = store.get_all_templates()
|
||||
responses = [
|
||||
AudioTemplateResponse(
|
||||
id=t.id, name=t.name, engine_type=t.engine_type,
|
||||
engine_config=t.engine_config, tags=t.tags,
|
||||
id=t.id,
|
||||
name=t.name,
|
||||
engine_type=t.engine_type,
|
||||
engine_config=t.engine_config,
|
||||
tags=t.tags,
|
||||
created_at=t.created_at,
|
||||
updated_at=t.updated_at, description=t.description,
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
)
|
||||
for t in templates
|
||||
]
|
||||
@@ -50,7 +62,12 @@ async def list_audio_templates(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.post("/api/v1/audio-templates", response_model=AudioTemplateResponse, tags=["Audio Templates"], status_code=201)
|
||||
@router.post(
|
||||
"/api/v1/audio-templates",
|
||||
response_model=AudioTemplateResponse,
|
||||
tags=["Audio Templates"],
|
||||
status_code=201,
|
||||
)
|
||||
async def create_audio_template(
|
||||
data: AudioTemplateCreate,
|
||||
_auth: AuthRequired,
|
||||
@@ -59,16 +76,22 @@ async def create_audio_template(
|
||||
"""Create a new audio capture template."""
|
||||
try:
|
||||
template = store.create_template(
|
||||
name=data.name, engine_type=data.engine_type,
|
||||
engine_config=data.engine_config, description=data.description,
|
||||
name=data.name,
|
||||
engine_type=data.engine_type,
|
||||
engine_config=data.engine_config,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("audio_template", "created", template.id)
|
||||
return AudioTemplateResponse(
|
||||
id=template.id, name=template.name, engine_type=template.engine_type,
|
||||
engine_config=template.engine_config, tags=template.tags,
|
||||
id=template.id,
|
||||
name=template.name,
|
||||
engine_type=template.engine_type,
|
||||
engine_config=template.engine_config,
|
||||
tags=template.tags,
|
||||
created_at=template.created_at,
|
||||
updated_at=template.updated_at, description=template.description,
|
||||
updated_at=template.updated_at,
|
||||
description=template.description,
|
||||
)
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
@@ -80,7 +103,11 @@ async def create_audio_template(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/api/v1/audio-templates/{template_id}", response_model=AudioTemplateResponse, tags=["Audio Templates"])
|
||||
@router.get(
|
||||
"/api/v1/audio-templates/{template_id}",
|
||||
response_model=AudioTemplateResponse,
|
||||
tags=["Audio Templates"],
|
||||
)
|
||||
async def get_audio_template(
|
||||
template_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -92,14 +119,22 @@ async def get_audio_template(
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail=f"Audio template {template_id} not found")
|
||||
return AudioTemplateResponse(
|
||||
id=t.id, name=t.name, engine_type=t.engine_type,
|
||||
engine_config=t.engine_config, tags=t.tags,
|
||||
id=t.id,
|
||||
name=t.name,
|
||||
engine_type=t.engine_type,
|
||||
engine_config=t.engine_config,
|
||||
tags=t.tags,
|
||||
created_at=t.created_at,
|
||||
updated_at=t.updated_at, description=t.description,
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/v1/audio-templates/{template_id}", response_model=AudioTemplateResponse, tags=["Audio Templates"])
|
||||
@router.put(
|
||||
"/api/v1/audio-templates/{template_id}",
|
||||
response_model=AudioTemplateResponse,
|
||||
tags=["Audio Templates"],
|
||||
)
|
||||
async def update_audio_template(
|
||||
template_id: str,
|
||||
data: AudioTemplateUpdate,
|
||||
@@ -109,16 +144,23 @@ async def update_audio_template(
|
||||
"""Update an audio template."""
|
||||
try:
|
||||
t = store.update_template(
|
||||
template_id=template_id, name=data.name,
|
||||
engine_type=data.engine_type, engine_config=data.engine_config,
|
||||
description=data.description, tags=data.tags,
|
||||
template_id=template_id,
|
||||
name=data.name,
|
||||
engine_type=data.engine_type,
|
||||
engine_config=data.engine_config,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("audio_template", "updated", template_id)
|
||||
return AudioTemplateResponse(
|
||||
id=t.id, name=t.name, engine_type=t.engine_type,
|
||||
engine_config=t.engine_config, tags=t.tags,
|
||||
id=t.id,
|
||||
name=t.name,
|
||||
engine_type=t.engine_type,
|
||||
engine_config=t.engine_config,
|
||||
tags=t.tags,
|
||||
created_at=t.created_at,
|
||||
updated_at=t.updated_at, description=t.description,
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
)
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
@@ -155,7 +197,10 @@ async def delete_audio_template(
|
||||
|
||||
# ===== AUDIO ENGINE ENDPOINTS =====
|
||||
|
||||
@router.get("/api/v1/audio-engines", response_model=AudioEngineListResponse, tags=["Audio Templates"])
|
||||
|
||||
@router.get(
|
||||
"/api/v1/audio-engines", response_model=AudioEngineListResponse, tags=["Audio Templates"]
|
||||
)
|
||||
async def list_audio_engines(_auth: AuthRequired):
|
||||
"""List all registered audio capture engines."""
|
||||
try:
|
||||
@@ -195,7 +240,8 @@ async def test_audio_template_ws(
|
||||
Auth via ?token=<api_key>. Device specified via ?device_index=N&is_loopback=0|1.
|
||||
Streams AudioAnalysis snapshots as JSON at ~20 Hz.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
return
|
||||
@@ -214,13 +260,17 @@ async def test_audio_template_ws(
|
||||
loopback = is_loopback != 0
|
||||
|
||||
try:
|
||||
stream = audio_mgr.acquire(device_index, loopback, template.engine_type, template.engine_config)
|
||||
stream = audio_mgr.acquire(
|
||||
device_index, loopback, template.engine_type, template.engine_config
|
||||
)
|
||||
except RuntimeError as e:
|
||||
await websocket.close(code=4003, reason=str(e))
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
logger.info(f"Audio template test WS connected: template={template_id} device={device_index} loopback={loopback}")
|
||||
logger.info(
|
||||
f"Audio template test WS connected: template={template_id} device={device_index} loopback={loopback}"
|
||||
)
|
||||
|
||||
last_ts = 0.0
|
||||
try:
|
||||
@@ -228,13 +278,15 @@ async def test_audio_template_ws(
|
||||
analysis = stream.get_latest_analysis()
|
||||
if analysis is not None and analysis.timestamp != last_ts:
|
||||
last_ts = analysis.timestamp
|
||||
await websocket.send_json({
|
||||
"spectrum": analysis.spectrum.tolist(),
|
||||
"rms": round(analysis.rms, 4),
|
||||
"peak": round(analysis.peak, 4),
|
||||
"beat": analysis.beat,
|
||||
"beat_intensity": round(analysis.beat_intensity, 4),
|
||||
})
|
||||
await websocket.send_json(
|
||||
{
|
||||
"spectrum": analysis.spectrum.tolist(),
|
||||
"rms": round(analysis.rms, 4),
|
||||
"peak": round(analysis.peak, 4),
|
||||
"beat": analysis.beat,
|
||||
"beat_intensity": round(analysis.beat_intensity, 4),
|
||||
}
|
||||
)
|
||||
await asyncio.sleep(0.05)
|
||||
except WebSocketDisconnect:
|
||||
logger.debug("Audio template test WebSocket disconnected")
|
||||
+10
-10
@@ -4,22 +4,22 @@ import secrets
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_automation_engine,
|
||||
get_automation_store,
|
||||
get_scene_preset_store,
|
||||
)
|
||||
from wled_controller.api.schemas.automations import (
|
||||
from ledgrab.api.schemas.automations import (
|
||||
AutomationCreate,
|
||||
AutomationListResponse,
|
||||
AutomationResponse,
|
||||
AutomationUpdate,
|
||||
RuleSchema,
|
||||
)
|
||||
from wled_controller.core.automations.automation_engine import AutomationEngine
|
||||
from wled_controller.storage.automation import (
|
||||
from ledgrab.core.automations.automation_engine import AutomationEngine
|
||||
from ledgrab.storage.automation import (
|
||||
ApplicationRule,
|
||||
DisplayStateRule,
|
||||
HomeAssistantRule,
|
||||
@@ -30,10 +30,10 @@ from wled_controller.storage.automation import (
|
||||
TimeOfDayRule,
|
||||
WebhookRule,
|
||||
)
|
||||
from wled_controller.storage.automation_store import AutomationStore
|
||||
from wled_controller.storage.scene_preset_store import ScenePresetStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.automation_store import AutomationStore
|
||||
from ledgrab.storage.scene_preset_store import ScenePresetStore
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -97,7 +97,7 @@ def _automation_to_response(
|
||||
for r in automation.rules:
|
||||
if isinstance(r, WebhookRule) and r.token:
|
||||
# Prefer configured external URL, fall back to request base URL
|
||||
from wled_controller.api.routes.system import load_external_url
|
||||
from ledgrab.api.routes.system import load_external_url
|
||||
|
||||
ext = load_external_url()
|
||||
if ext:
|
||||
+26
-13
@@ -15,20 +15,20 @@ from pathlib import Path
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import get_asset_store, get_auto_backup_engine, get_database
|
||||
from wled_controller.api.schemas.system import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import get_asset_store, get_auto_backup_engine, get_database
|
||||
from ledgrab.api.schemas.system import (
|
||||
AutoBackupSettings,
|
||||
AutoBackupStatusResponse,
|
||||
BackupFileInfo,
|
||||
BackupListResponse,
|
||||
RestoreResponse,
|
||||
)
|
||||
from wled_controller.config import get_config
|
||||
from wled_controller.core.backup.auto_backup import AutoBackupEngine
|
||||
from wled_controller.storage.asset_store import AssetStore
|
||||
from wled_controller.storage.database import Database, freeze_writes
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.config import get_config
|
||||
from ledgrab.core.backup.auto_backup import AutoBackupEngine
|
||||
from ledgrab.storage.asset_store import AssetStore
|
||||
from ledgrab.storage.database import Database, freeze_writes
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -42,11 +42,17 @@ def _schedule_restart() -> None:
|
||||
|
||||
def _restart():
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
if sys.platform == "win32":
|
||||
subprocess.Popen(
|
||||
["powershell", "-ExecutionPolicy", "Bypass", "-File",
|
||||
str(_SERVER_DIR / "restart.ps1")],
|
||||
[
|
||||
"powershell",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
str(_SERVER_DIR / "restart.ps1"),
|
||||
],
|
||||
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
)
|
||||
else:
|
||||
@@ -71,6 +77,7 @@ def backup_config(
|
||||
):
|
||||
"""Download a full backup as a .zip containing the database and asset files."""
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
|
||||
@@ -95,6 +102,7 @@ def backup_config(
|
||||
zip_buffer.seek(0)
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
|
||||
filename = f"ledgrab-backup-{timestamp}.zip"
|
||||
|
||||
@@ -129,7 +137,9 @@ async def restore_config(
|
||||
is_sqlite = raw[:16].startswith(b"SQLite format 3")
|
||||
|
||||
if not is_zip and not is_sqlite:
|
||||
raise HTTPException(status_code=400, detail="Not a valid backup file (expected .zip or .db)")
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Not a valid backup file (expected .zip or .db)"
|
||||
)
|
||||
|
||||
if is_zip:
|
||||
# Extract DB and assets from ZIP
|
||||
@@ -160,6 +170,7 @@ async def restore_config(
|
||||
tmp_path = Path(tmp.name)
|
||||
|
||||
try:
|
||||
|
||||
def _restore():
|
||||
db.restore_from(tmp_path)
|
||||
|
||||
@@ -181,7 +192,8 @@ async def restore_config(
|
||||
@router.post("/api/v1/system/restart", tags=["System"])
|
||||
def restart_server(_: AuthRequired):
|
||||
"""Schedule a server restart and return immediately."""
|
||||
from wled_controller.server_ref import _broadcast_restarting
|
||||
from ledgrab.server_ref import _broadcast_restarting
|
||||
|
||||
_broadcast_restarting()
|
||||
_schedule_restart()
|
||||
return {"status": "restarting"}
|
||||
@@ -190,7 +202,8 @@ def restart_server(_: AuthRequired):
|
||||
@router.post("/api/v1/system/shutdown", tags=["System"])
|
||||
def shutdown_server(_: AuthRequired):
|
||||
"""Gracefully shut down the server."""
|
||||
from wled_controller.server_ref import request_shutdown
|
||||
from ledgrab.server_ref import request_shutdown
|
||||
|
||||
request_shutdown()
|
||||
return {"status": "shutting_down"}
|
||||
|
||||
+49
-21
@@ -6,27 +6,27 @@ import uuid as _uuid
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_color_strip_store,
|
||||
get_cspt_store,
|
||||
get_device_store,
|
||||
get_processor_manager,
|
||||
)
|
||||
from wled_controller.api.schemas.filters import FilterInstanceSchema
|
||||
from wled_controller.api.schemas.color_strip_processing import (
|
||||
from ledgrab.api.schemas.filters import FilterInstanceSchema
|
||||
from ledgrab.api.schemas.color_strip_processing import (
|
||||
ColorStripProcessingTemplateCreate,
|
||||
ColorStripProcessingTemplateListResponse,
|
||||
ColorStripProcessingTemplateResponse,
|
||||
ColorStripProcessingTemplateUpdate,
|
||||
)
|
||||
from wled_controller.core.filters import FilterInstance
|
||||
from wled_controller.storage.color_strip_processing_template_store import ColorStripProcessingTemplateStore
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.core.filters import FilterInstance
|
||||
from ledgrab.storage.color_strip_processing_template_store import ColorStripProcessingTemplateStore
|
||||
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||
from ledgrab.storage import DeviceStore
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -46,7 +46,11 @@ def _cspt_to_response(t) -> ColorStripProcessingTemplateResponse:
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/color-strip-processing-templates", response_model=ColorStripProcessingTemplateListResponse, tags=["Color Strip Processing"])
|
||||
@router.get(
|
||||
"/api/v1/color-strip-processing-templates",
|
||||
response_model=ColorStripProcessingTemplateListResponse,
|
||||
tags=["Color Strip Processing"],
|
||||
)
|
||||
async def list_cspt(
|
||||
_auth: AuthRequired,
|
||||
store: ColorStripProcessingTemplateStore = Depends(get_cspt_store),
|
||||
@@ -61,7 +65,12 @@ async def list_cspt(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.post("/api/v1/color-strip-processing-templates", response_model=ColorStripProcessingTemplateResponse, tags=["Color Strip Processing"], status_code=201)
|
||||
@router.post(
|
||||
"/api/v1/color-strip-processing-templates",
|
||||
response_model=ColorStripProcessingTemplateResponse,
|
||||
tags=["Color Strip Processing"],
|
||||
status_code=201,
|
||||
)
|
||||
async def create_cspt(
|
||||
data: ColorStripProcessingTemplateCreate,
|
||||
_auth: AuthRequired,
|
||||
@@ -88,7 +97,11 @@ async def create_cspt(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/api/v1/color-strip-processing-templates/{template_id}", response_model=ColorStripProcessingTemplateResponse, tags=["Color Strip Processing"])
|
||||
@router.get(
|
||||
"/api/v1/color-strip-processing-templates/{template_id}",
|
||||
response_model=ColorStripProcessingTemplateResponse,
|
||||
tags=["Color Strip Processing"],
|
||||
)
|
||||
async def get_cspt(
|
||||
template_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -99,10 +112,16 @@ async def get_cspt(
|
||||
template = store.get_template(template_id)
|
||||
return _cspt_to_response(template)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail=f"Color strip processing template {template_id} not found")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Color strip processing template {template_id} not found"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/v1/color-strip-processing-templates/{template_id}", response_model=ColorStripProcessingTemplateResponse, tags=["Color Strip Processing"])
|
||||
@router.put(
|
||||
"/api/v1/color-strip-processing-templates/{template_id}",
|
||||
response_model=ColorStripProcessingTemplateResponse,
|
||||
tags=["Color Strip Processing"],
|
||||
)
|
||||
async def update_cspt(
|
||||
template_id: str,
|
||||
data: ColorStripProcessingTemplateUpdate,
|
||||
@@ -111,7 +130,11 @@ async def update_cspt(
|
||||
):
|
||||
"""Update a color strip processing template."""
|
||||
try:
|
||||
filters = [FilterInstance(f.filter_id, f.options) for f in data.filters] if data.filters is not None else None
|
||||
filters = (
|
||||
[FilterInstance(f.filter_id, f.options) for f in data.filters]
|
||||
if data.filters is not None
|
||||
else None
|
||||
)
|
||||
template = store.update_template(
|
||||
template_id=template_id,
|
||||
name=data.name,
|
||||
@@ -131,7 +154,11 @@ async def update_cspt(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.delete("/api/v1/color-strip-processing-templates/{template_id}", status_code=204, tags=["Color Strip Processing"])
|
||||
@router.delete(
|
||||
"/api/v1/color-strip-processing-templates/{template_id}",
|
||||
status_code=204,
|
||||
tags=["Color Strip Processing"],
|
||||
)
|
||||
async def delete_cspt(
|
||||
template_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -147,7 +174,7 @@ async def delete_cspt(
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Cannot delete: template is referenced by: {names}. "
|
||||
"Please reassign before deleting.",
|
||||
"Please reassign before deleting.",
|
||||
)
|
||||
store.delete_template(template_id)
|
||||
fire_entity_event("cspt", "deleted", template_id)
|
||||
@@ -165,6 +192,7 @@ async def delete_cspt(
|
||||
|
||||
# ── Test / Preview WebSocket ──────────────────────────────────────────
|
||||
|
||||
|
||||
@router.websocket("/api/v1/color-strip-processing-templates/{template_id}/test/ws")
|
||||
async def test_cspt_ws(
|
||||
websocket: WebSocket,
|
||||
@@ -179,9 +207,9 @@ async def test_cspt_ws(
|
||||
Takes an input CSS source, applies the CSPT filter chain, and streams
|
||||
the processed RGB frames. Auth via ``?token=<api_key>``.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from wled_controller.core.filters import FilterRegistry
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
from ledgrab.core.filters import FilterRegistry
|
||||
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
+42
-42
@@ -9,8 +9,8 @@ from typing import Annotated
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_color_strip_store,
|
||||
get_device_store,
|
||||
@@ -20,7 +20,7 @@ from wled_controller.api.dependencies import (
|
||||
get_processor_manager,
|
||||
get_template_store,
|
||||
)
|
||||
from wled_controller.api.schemas.color_strip_sources import (
|
||||
from ledgrab.api.schemas.color_strip_sources import (
|
||||
ApiInputCSSResponse,
|
||||
AudioCSSResponse,
|
||||
CandlelightCSSResponse,
|
||||
@@ -47,17 +47,17 @@ from wled_controller.api.schemas.color_strip_sources import (
|
||||
StaticCSSResponse,
|
||||
WeatherCSSResponse,
|
||||
)
|
||||
from wled_controller.api.schemas.devices import (
|
||||
from ledgrab.api.schemas.devices import (
|
||||
Calibration as CalibrationSchema,
|
||||
CalibrationTestModeResponse,
|
||||
)
|
||||
from wled_controller.core.capture.calibration import (
|
||||
from ledgrab.core.capture.calibration import (
|
||||
calibration_from_dict,
|
||||
calibration_to_dict,
|
||||
)
|
||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.storage.color_strip_source import (
|
||||
from ledgrab.core.capture.screen_capture import get_available_displays
|
||||
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||
from ledgrab.storage.color_strip_source import (
|
||||
AdvancedPictureColorStripSource,
|
||||
ApiInputColorStripSource,
|
||||
AudioColorStripSource,
|
||||
@@ -76,17 +76,17 @@ from wled_controller.storage.color_strip_source import (
|
||||
StaticColorStripSource,
|
||||
WeatherColorStripSource,
|
||||
)
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.template_store import TemplateStore
|
||||
from wled_controller.storage.picture_source import (
|
||||
from ledgrab.storage import DeviceStore
|
||||
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||
from ledgrab.storage.template_store import TemplateStore
|
||||
from ledgrab.storage.picture_source import (
|
||||
ProcessedPictureSource,
|
||||
ScreenCapturePictureSource,
|
||||
)
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -371,7 +371,7 @@ async def create_color_strip_source(
|
||||
if data.source_type == "composite" and kwargs.get("layers"):
|
||||
child_ids = [ly.get("source_id", "") for ly in kwargs["layers"] if ly.get("source_id")]
|
||||
# No parent_id yet (new source), just check depth
|
||||
from wled_controller.storage.color_strip_store import MAX_COMPOSITE_DEPTH
|
||||
from ledgrab.storage.color_strip_store import MAX_COMPOSITE_DEPTH
|
||||
|
||||
for cid in child_ids:
|
||||
depth = store.get_nesting_depth(cid)
|
||||
@@ -524,19 +524,19 @@ async def test_key_colors_source(
|
||||
pp_template_store=Depends(get_pp_template_store),
|
||||
):
|
||||
"""Test a key_colors source: capture a frame, extract colors from each rectangle."""
|
||||
from wled_controller.storage.color_strip_source import KeyColorsColorStripSource
|
||||
from wled_controller.core.capture.screen_capture import (
|
||||
from ledgrab.storage.color_strip_source import KeyColorsColorStripSource
|
||||
from ledgrab.core.capture.screen_capture import (
|
||||
calculate_average_color,
|
||||
calculate_dominant_color,
|
||||
calculate_median_color,
|
||||
)
|
||||
from wled_controller.core.capture_engines import EngineRegistry
|
||||
from wled_controller.core.filters import FilterRegistry, ImagePool
|
||||
from wled_controller.storage.picture_source import (
|
||||
from ledgrab.core.capture_engines import EngineRegistry
|
||||
from ledgrab.core.filters import FilterRegistry, ImagePool
|
||||
from ledgrab.storage.picture_source import (
|
||||
ScreenCapturePictureSource,
|
||||
StaticImagePictureSource,
|
||||
)
|
||||
from wled_controller.utils.image_codec import encode_jpeg_data_uri
|
||||
from ledgrab.utils.image_codec import encode_jpeg_data_uri
|
||||
|
||||
stream = None
|
||||
try:
|
||||
@@ -553,10 +553,10 @@ async def test_key_colors_source(
|
||||
chain = source_store.resolve_stream_chain(source.picture_source_id)
|
||||
raw_stream = chain["raw_stream"]
|
||||
|
||||
from wled_controller.utils.image_codec import load_image_file
|
||||
from ledgrab.utils.image_codec import load_image_file
|
||||
|
||||
if isinstance(raw_stream, StaticImagePictureSource):
|
||||
from wled_controller.api.dependencies import get_asset_store as _get_asset_store
|
||||
from ledgrab.api.dependencies import get_asset_store as _get_asset_store
|
||||
|
||||
asset_store = _get_asset_store()
|
||||
image_path = (
|
||||
@@ -681,15 +681,15 @@ async def test_key_colors_ws(
|
||||
"""WebSocket for real-time key_colors test preview with frame + rectangle overlay."""
|
||||
import json as ws_json
|
||||
import time as ws_time
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from wled_controller.storage.color_strip_source import KeyColorsColorStripSource
|
||||
from wled_controller.core.capture.screen_capture import (
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
from ledgrab.storage.color_strip_source import KeyColorsColorStripSource
|
||||
from ledgrab.core.capture.screen_capture import (
|
||||
calculate_average_color,
|
||||
calculate_dominant_color,
|
||||
calculate_median_color,
|
||||
)
|
||||
from wled_controller.storage.picture_source import ScreenCapturePictureSource
|
||||
from wled_controller.utils.image_codec import encode_jpeg_data_uri, resize_down
|
||||
from ledgrab.storage.picture_source import ScreenCapturePictureSource
|
||||
from ledgrab.utils.image_codec import encode_jpeg_data_uri, resize_down
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
@@ -1095,7 +1095,7 @@ async def notify_source(
|
||||
@router.get("/api/v1/color-strip-sources/os-notifications/history", tags=["Color Strip Sources"])
|
||||
async def os_notification_history(_auth: AuthRequired):
|
||||
"""Return recent OS notification capture history (newest first)."""
|
||||
from wled_controller.core.processing.os_notification_listener import (
|
||||
from ledgrab.core.processing.os_notification_listener import (
|
||||
get_os_notification_listener,
|
||||
)
|
||||
|
||||
@@ -1139,7 +1139,7 @@ async def preview_color_strip_ws(
|
||||
Subsequent text messages are treated as config updates: if the source_type
|
||||
changed the old stream is replaced; otherwise ``update_source()`` is used.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
@@ -1168,7 +1168,7 @@ async def preview_color_strip_ws(
|
||||
|
||||
def _build_source(config: dict):
|
||||
"""Build a ColorStripSource from a raw config dict, injecting synthetic id/name."""
|
||||
from wled_controller.storage.color_strip_source import ColorStripSource
|
||||
from ledgrab.storage.color_strip_source import ColorStripSource
|
||||
|
||||
config.setdefault("id", "__preview__")
|
||||
config.setdefault("name", "__preview__")
|
||||
@@ -1176,7 +1176,7 @@ async def preview_color_strip_ws(
|
||||
|
||||
def _create_stream(source):
|
||||
"""Instantiate and start the appropriate stream class for *source*."""
|
||||
from wled_controller.core.processing.color_strip_stream_manager import _SIMPLE_STREAM_MAP
|
||||
from ledgrab.core.processing.color_strip_stream_manager import _SIMPLE_STREAM_MAP
|
||||
|
||||
stream_cls = _SIMPLE_STREAM_MAP.get(source.source_type)
|
||||
if not stream_cls:
|
||||
@@ -1185,7 +1185,7 @@ async def preview_color_strip_ws(
|
||||
# Inject gradient store for palette resolution
|
||||
if hasattr(s, "set_gradient_store"):
|
||||
try:
|
||||
from wled_controller.api.dependencies import get_gradient_store
|
||||
from ledgrab.api.dependencies import get_gradient_store
|
||||
|
||||
s.set_gradient_store(get_gradient_store())
|
||||
except Exception:
|
||||
@@ -1283,7 +1283,7 @@ async def preview_color_strip_ws(
|
||||
|
||||
# Handle "fire" command for notification streams
|
||||
if new_config.get("action") == "fire":
|
||||
from wled_controller.core.processing.notification_stream import (
|
||||
from ledgrab.core.processing.notification_stream import (
|
||||
NotificationColorStripStream,
|
||||
)
|
||||
|
||||
@@ -1349,7 +1349,7 @@ async def css_api_input_ws(
|
||||
Auth via ?token=<api_key>. Accepts JSON frames ({"colors": [[R,G,B], ...]})
|
||||
or binary frames (raw RGBRGB... bytes, 3 bytes per LED).
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
@@ -1391,7 +1391,7 @@ async def css_api_input_ws(
|
||||
if "segments" in data:
|
||||
# Segment-based path — validate and push
|
||||
try:
|
||||
from wled_controller.api.schemas.color_strip_sources import SegmentPayload
|
||||
from ledgrab.api.schemas.color_strip_sources import SegmentPayload
|
||||
|
||||
seg_dicts = [SegmentPayload(**s).model_dump() for s in data["segments"]]
|
||||
except Exception as e:
|
||||
@@ -1460,7 +1460,7 @@ async def test_color_strip_ws(
|
||||
First message is JSON metadata (source_type, led_count, calibration segments).
|
||||
Subsequent messages are binary RGB frames (``led_count * 3`` bytes).
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
@@ -1506,9 +1506,9 @@ async def test_color_strip_ws(
|
||||
logger.info(f"CSS test WebSocket connected for {source_id} (fps={fps})")
|
||||
|
||||
try:
|
||||
from wled_controller.core.processing.composite_stream import CompositeColorStripStream
|
||||
from ledgrab.core.processing.composite_stream import CompositeColorStripStream
|
||||
|
||||
from wled_controller.core.processing.api_input_stream import ApiInputColorStripStream
|
||||
from ledgrab.core.processing.api_input_stream import ApiInputColorStripStream
|
||||
|
||||
is_api_input = isinstance(stream, ApiInputColorStripStream)
|
||||
_last_push_gen = 0 # track api_input push generation to skip unchanged frames
|
||||
@@ -1643,7 +1643,7 @@ async def test_color_strip_ws(
|
||||
try:
|
||||
frame = _frame_live.get_latest_frame()
|
||||
if frame is not None and frame.image is not None:
|
||||
from wled_controller.utils.image_codec import encode_jpeg
|
||||
from ledgrab.utils.image_codec import encode_jpeg
|
||||
import cv2 as _cv2
|
||||
|
||||
img = frame.image
|
||||
+12
-12
@@ -3,19 +3,19 @@
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.core.devices.led_client import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.core.devices.led_client import (
|
||||
get_all_providers,
|
||||
get_device_capabilities,
|
||||
get_provider,
|
||||
)
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_device_store,
|
||||
get_output_target_store,
|
||||
get_processor_manager,
|
||||
)
|
||||
from wled_controller.api.schemas.devices import (
|
||||
from ledgrab.api.schemas.devices import (
|
||||
BrightnessRequest,
|
||||
DeviceCreate,
|
||||
DeviceListResponse,
|
||||
@@ -28,10 +28,10 @@ from wled_controller.api.schemas.devices import (
|
||||
OpenRGBZonesResponse,
|
||||
PowerRequest,
|
||||
)
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||
from ledgrab.storage import DeviceStore
|
||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -300,14 +300,14 @@ async def get_openrgb_zones(
|
||||
"""List available zones on an OpenRGB device."""
|
||||
import asyncio
|
||||
|
||||
from wled_controller.core.devices.openrgb_client import parse_openrgb_url
|
||||
from ledgrab.core.devices.openrgb_client import parse_openrgb_url
|
||||
|
||||
host, port, device_index, _zones = parse_openrgb_url(url)
|
||||
|
||||
def _fetch_zones():
|
||||
from openrgb import OpenRGBClient
|
||||
|
||||
client = OpenRGBClient(host, port, name="WLED Controller (zones)")
|
||||
client = OpenRGBClient(host, port, name="LedGrab (zones)")
|
||||
try:
|
||||
devices = client.devices
|
||||
if device_index >= len(devices):
|
||||
@@ -742,7 +742,7 @@ async def device_ws_stream(
|
||||
Wire format: [brightness_byte][R G B R G B ...]
|
||||
Auth via ?token=<api_key>.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
@@ -760,7 +760,7 @@ async def device_ws_stream(
|
||||
|
||||
await websocket.accept()
|
||||
|
||||
from wled_controller.core.devices.ws_client import get_ws_broadcaster
|
||||
from ledgrab.core.devices.ws_client import get_ws_broadcaster
|
||||
|
||||
broadcaster = get_ws_broadcaster()
|
||||
broadcaster.add_client(device_id, websocket)
|
||||
+14
-14
@@ -10,14 +10,14 @@ from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_database,
|
||||
get_game_integration_store,
|
||||
get_game_event_bus,
|
||||
)
|
||||
from wled_controller.api.schemas.game_integration import (
|
||||
from ledgrab.api.schemas.game_integration import (
|
||||
AdapterInfoResponse,
|
||||
AdapterListResponse,
|
||||
ApplyPresetRequest,
|
||||
@@ -34,13 +34,13 @@ from wled_controller.api.schemas.game_integration import (
|
||||
PresetListResponse,
|
||||
RecentEventsResponse,
|
||||
)
|
||||
from wled_controller.core.game_integration.adapter_registry import AdapterRegistry
|
||||
from wled_controller.core.game_integration.event_bus import GameEventBus
|
||||
from wled_controller.core.game_integration.events import GameEvent
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from wled_controller.storage.game_integration import EventMapping
|
||||
from wled_controller.storage.game_integration_store import GameIntegrationStore
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.game_integration.adapter_registry import AdapterRegistry
|
||||
from ledgrab.core.game_integration.event_bus import GameEventBus
|
||||
from ledgrab.core.game_integration.events import GameEvent
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.game_integration import EventMapping
|
||||
from ledgrab.storage.game_integration_store import GameIntegrationStore
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -135,7 +135,7 @@ def _cleanup_state(integration_id: str) -> None:
|
||||
|
||||
def _config_to_response(config: Any) -> GameIntegrationResponse:
|
||||
"""Convert a GameIntegrationConfig to its API response."""
|
||||
from wled_controller.api.schemas.game_integration import EventMappingSchema
|
||||
from ledgrab.api.schemas.game_integration import EventMappingSchema
|
||||
|
||||
return GameIntegrationResponse(
|
||||
id=config.id,
|
||||
@@ -171,7 +171,7 @@ def _config_to_response(config: Any) -> GameIntegrationResponse:
|
||||
)
|
||||
async def list_presets(_auth: AuthRequired):
|
||||
"""List all available built-in effect presets."""
|
||||
from wled_controller.core.game_integration.presets import get_all_presets
|
||||
from ledgrab.core.game_integration.presets import get_all_presets
|
||||
|
||||
presets = get_all_presets()
|
||||
responses = [
|
||||
@@ -554,7 +554,7 @@ async def apply_preset(
|
||||
If replace=true, replaces all existing mappings.
|
||||
If replace=false (default), appends preset mappings to existing ones.
|
||||
"""
|
||||
from wled_controller.core.game_integration.presets import get_preset
|
||||
from ledgrab.core.game_integration.presets import get_preset
|
||||
|
||||
try:
|
||||
config = store.get_integration(integration_id)
|
||||
@@ -619,7 +619,7 @@ async def auto_setup_integration(
|
||||
)
|
||||
|
||||
# Determine server URL
|
||||
from wled_controller.api.routes.system_settings import load_external_url
|
||||
from ledgrab.api.routes.system_settings import load_external_url
|
||||
|
||||
db = get_database()
|
||||
server_url = load_external_url(db)
|
||||
+18
-13
@@ -2,23 +2,23 @@
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_color_strip_store,
|
||||
get_gradient_store,
|
||||
)
|
||||
from wled_controller.api.schemas.gradients import (
|
||||
from ledgrab.api.schemas.gradients import (
|
||||
GradientCreate,
|
||||
GradientListResponse,
|
||||
GradientResponse,
|
||||
GradientUpdate,
|
||||
)
|
||||
from wled_controller.storage.gradient import Gradient
|
||||
from wled_controller.storage.gradient_store import GradientStore
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.storage.gradient import Gradient
|
||||
from ledgrab.storage.gradient_store import GradientStore
|
||||
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -51,7 +51,9 @@ async def list_gradients(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/v1/gradients", response_model=GradientResponse, status_code=201, tags=["Gradients"])
|
||||
@router.post(
|
||||
"/api/v1/gradients", response_model=GradientResponse, status_code=201, tags=["Gradients"]
|
||||
)
|
||||
async def create_gradient(
|
||||
data: GradientCreate,
|
||||
_auth: AuthRequired,
|
||||
@@ -109,7 +111,12 @@ async def update_gradient(
|
||||
raise HTTPException(status_code=status, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/v1/gradients/{gradient_id}/clone", response_model=GradientResponse, status_code=201, tags=["Gradients"])
|
||||
@router.post(
|
||||
"/api/v1/gradients/{gradient_id}/clone",
|
||||
response_model=GradientResponse,
|
||||
status_code=201,
|
||||
tags=["Gradients"],
|
||||
)
|
||||
async def clone_gradient(
|
||||
gradient_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -143,9 +150,7 @@ async def delete_gradient(
|
||||
# Check references
|
||||
for source in css_store.get_all_sources():
|
||||
if getattr(source, "gradient_id", None) == gradient_id:
|
||||
raise ValueError(
|
||||
f"Cannot delete: referenced by color strip source '{source.name}'"
|
||||
)
|
||||
raise ValueError(f"Cannot delete: referenced by color strip source '{source.name}'")
|
||||
store.delete_gradient(gradient_id)
|
||||
fire_entity_event("gradient", "deleted", gradient_id)
|
||||
except (ValueError, EntityNotFoundError) as e:
|
||||
+9
-9
@@ -5,13 +5,13 @@ import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_ha_manager,
|
||||
get_ha_store,
|
||||
)
|
||||
from wled_controller.api.schemas.home_assistant import (
|
||||
from ledgrab.api.schemas.home_assistant import (
|
||||
HomeAssistantConnectionStatus,
|
||||
HomeAssistantEntityListResponse,
|
||||
HomeAssistantEntityResponse,
|
||||
@@ -22,12 +22,12 @@ from wled_controller.api.schemas.home_assistant import (
|
||||
HomeAssistantStatusResponse,
|
||||
HomeAssistantTestResponse,
|
||||
)
|
||||
from wled_controller.core.home_assistant.ha_manager import HomeAssistantManager
|
||||
from wled_controller.core.home_assistant.ha_runtime import HARuntime
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from wled_controller.storage.home_assistant_source import HomeAssistantSource
|
||||
from wled_controller.storage.home_assistant_store import HomeAssistantStore
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.home_assistant.ha_manager import HomeAssistantManager
|
||||
from ledgrab.core.home_assistant.ha_runtime import HARuntime
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.home_assistant_source import HomeAssistantSource
|
||||
from ledgrab.storage.home_assistant_store import HomeAssistantStore
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
+8
-8
@@ -5,13 +5,13 @@ import asyncio
|
||||
import aiomqtt
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_mqtt_manager,
|
||||
get_mqtt_store,
|
||||
)
|
||||
from wled_controller.api.schemas.mqtt import (
|
||||
from ledgrab.api.schemas.mqtt import (
|
||||
MQTTConnectionStatus,
|
||||
MQTTSourceCreate,
|
||||
MQTTSourceListResponse,
|
||||
@@ -20,11 +20,11 @@ from wled_controller.api.schemas.mqtt import (
|
||||
MQTTStatusResponse,
|
||||
MQTTTestResponse,
|
||||
)
|
||||
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from wled_controller.storage.mqtt_source import MQTTSource
|
||||
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.mqtt.mqtt_manager import MQTTManager
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.mqtt_source import MQTTSource
|
||||
from ledgrab.storage.mqtt_source_store import MQTTSourceStore
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
+11
-11
@@ -5,14 +5,14 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, Depends
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_device_store,
|
||||
get_output_target_store,
|
||||
get_processor_manager,
|
||||
)
|
||||
from wled_controller.api.schemas.output_targets import (
|
||||
from ledgrab.api.schemas.output_targets import (
|
||||
HALightMappingSchema,
|
||||
HALightOutputTargetResponse,
|
||||
LedOutputTargetResponse,
|
||||
@@ -21,17 +21,17 @@ from wled_controller.api.schemas.output_targets import (
|
||||
OutputTargetResponse,
|
||||
OutputTargetUpdate,
|
||||
)
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.storage.bindable import BindableFloat
|
||||
from wled_controller.storage.wled_output_target import WledOutputTarget
|
||||
from wled_controller.storage.ha_light_output_target import (
|
||||
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||
from ledgrab.storage import DeviceStore
|
||||
from ledgrab.storage.bindable import BindableFloat
|
||||
from ledgrab.storage.wled_output_target import WledOutputTarget
|
||||
from ledgrab.storage.ha_light_output_target import (
|
||||
HALightMapping,
|
||||
HALightOutputTarget,
|
||||
)
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
+15
-15
@@ -5,30 +5,30 @@ Extracted from output_targets.py to keep files under 800 lines.
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
get_color_strip_store,
|
||||
get_output_target_store,
|
||||
get_picture_source_store,
|
||||
get_processor_manager,
|
||||
)
|
||||
from wled_controller.api.schemas.output_targets import (
|
||||
from ledgrab.api.schemas.output_targets import (
|
||||
BulkTargetRequest,
|
||||
BulkTargetResponse,
|
||||
TargetMetricsResponse,
|
||||
TargetProcessingState,
|
||||
)
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.color_strip_source import (
|
||||
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||
from ledgrab.core.capture.screen_capture import get_available_displays
|
||||
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||
from ledgrab.storage.color_strip_source import (
|
||||
AdvancedPictureColorStripSource,
|
||||
PictureColorStripSource,
|
||||
)
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
from wled_controller.storage.wled_output_target import WledOutputTarget
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||
from ledgrab.storage.wled_output_target import WledOutputTarget
|
||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -208,7 +208,7 @@ async def events_ws(
|
||||
token: str = Query(""),
|
||||
):
|
||||
"""WebSocket for real-time state change events. Auth via ?token=<api_key>."""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
@@ -272,7 +272,7 @@ async def start_target_overlay(
|
||||
):
|
||||
calibration = css.calibration
|
||||
# Resolve the display this CSS is capturing
|
||||
from wled_controller.api.routes.color_strip_sources import (
|
||||
from ledgrab.api.routes.color_strip_sources import (
|
||||
_resolve_display_index,
|
||||
)
|
||||
|
||||
@@ -348,7 +348,7 @@ async def ha_light_colors_ws(
|
||||
Streams: {"type": "colors_update", "colors": {entity_id: {r,g,b,hex}, ...}}
|
||||
at the target's update_rate.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
@@ -390,7 +390,7 @@ async def led_preview_ws(
|
||||
token: str = Query(""),
|
||||
):
|
||||
"""WebSocket for real-time LED strip preview. Sends binary RGB frames. Auth via ?token=<api_key>."""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
+9
-9
@@ -2,24 +2,24 @@
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_pattern_template_store,
|
||||
get_output_target_store,
|
||||
)
|
||||
from wled_controller.api.schemas.pattern_templates import (
|
||||
from ledgrab.api.schemas.pattern_templates import (
|
||||
PatternTemplateCreate,
|
||||
PatternTemplateListResponse,
|
||||
PatternTemplateResponse,
|
||||
PatternTemplateUpdate,
|
||||
)
|
||||
from wled_controller.api.schemas.output_targets import KeyColorRectangleSchema
|
||||
from wled_controller.storage.pattern_template import KeyColorRectangle
|
||||
from wled_controller.storage.pattern_template_store import PatternTemplateStore
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.api.schemas.output_targets import KeyColorRectangleSchema
|
||||
from ledgrab.storage.pattern_template import KeyColorRectangle
|
||||
from ledgrab.storage.pattern_template_store import PatternTemplateStore
|
||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
+29
-29
@@ -9,20 +9,20 @@ import numpy as np
|
||||
from fastapi import APIRouter, Body, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import Response
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_picture_source_store,
|
||||
get_output_target_store,
|
||||
get_pp_template_store,
|
||||
get_template_store,
|
||||
)
|
||||
from wled_controller.api.schemas.common import (
|
||||
from ledgrab.api.schemas.common import (
|
||||
CaptureImage,
|
||||
PerformanceMetrics,
|
||||
TemplateTestResponse,
|
||||
)
|
||||
from wled_controller.api.schemas.picture_sources import (
|
||||
from ledgrab.api.schemas.picture_sources import (
|
||||
ImageValidateRequest,
|
||||
ImageValidateResponse,
|
||||
PictureSourceCreate,
|
||||
@@ -35,20 +35,20 @@ from wled_controller.api.schemas.picture_sources import (
|
||||
StaticImagePictureSourceResponse,
|
||||
VideoPictureSourceResponse,
|
||||
)
|
||||
from wled_controller.core.capture_engines import EngineRegistry
|
||||
from wled_controller.core.filters import FilterRegistry, ImagePool
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.storage.template_store import TemplateStore
|
||||
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
from wled_controller.storage.picture_source import (
|
||||
from ledgrab.core.capture_engines import EngineRegistry
|
||||
from ledgrab.core.filters import FilterRegistry, ImagePool
|
||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||
from ledgrab.storage.template_store import TemplateStore
|
||||
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||
from ledgrab.storage.picture_source import (
|
||||
ProcessedPictureSource,
|
||||
ScreenCapturePictureSource,
|
||||
StaticImagePictureSource,
|
||||
VideoCaptureSource,
|
||||
)
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -142,7 +142,7 @@ async def validate_image(
|
||||
"""Validate an image source (URL or file path) and return a preview thumbnail."""
|
||||
try:
|
||||
|
||||
from wled_controller.utils.safe_source import validate_image_path, validate_image_url
|
||||
from ledgrab.utils.safe_source import validate_image_path, validate_image_url
|
||||
|
||||
source = data.image_source.strip()
|
||||
if not source:
|
||||
@@ -161,7 +161,7 @@ async def validate_image(
|
||||
img_bytes = path
|
||||
|
||||
def _process_image(src):
|
||||
from wled_controller.utils.image_codec import (
|
||||
from ledgrab.utils.image_codec import (
|
||||
encode_jpeg_data_uri,
|
||||
load_image_bytes,
|
||||
load_image_file,
|
||||
@@ -198,7 +198,7 @@ async def get_full_image(
|
||||
):
|
||||
"""Serve the full-resolution image for lightbox preview."""
|
||||
|
||||
from wled_controller.utils.safe_source import validate_image_path, validate_image_url
|
||||
from ledgrab.utils.safe_source import validate_image_path, validate_image_url
|
||||
|
||||
try:
|
||||
if source.startswith(("http://", "https://")):
|
||||
@@ -214,7 +214,7 @@ async def get_full_image(
|
||||
img_bytes = path
|
||||
|
||||
def _encode_full(src):
|
||||
from wled_controller.utils.image_codec import (
|
||||
from ledgrab.utils.image_codec import (
|
||||
encode_jpeg,
|
||||
load_image_bytes,
|
||||
load_image_file,
|
||||
@@ -375,9 +375,9 @@ async def get_video_thumbnail(
|
||||
store: PictureSourceStore = Depends(get_picture_source_store),
|
||||
):
|
||||
"""Get a thumbnail for a video picture source (first frame)."""
|
||||
from wled_controller.core.processing.video_stream import extract_thumbnail
|
||||
from wled_controller.storage.picture_source import VideoCaptureSource
|
||||
from wled_controller.utils.image_codec import encode_jpeg_data_uri, resize_down
|
||||
from ledgrab.core.processing.video_stream import extract_thumbnail
|
||||
from ledgrab.storage.picture_source import VideoCaptureSource
|
||||
from ledgrab.utils.image_codec import encode_jpeg_data_uri, resize_down
|
||||
|
||||
try:
|
||||
source = store.get_stream(stream_id)
|
||||
@@ -385,7 +385,7 @@ async def get_video_thumbnail(
|
||||
raise HTTPException(status_code=400, detail="Not a video source")
|
||||
|
||||
# Resolve video asset to file path
|
||||
from wled_controller.api.dependencies import get_asset_store as _get_asset_store
|
||||
from ledgrab.api.dependencies import get_asset_store as _get_asset_store
|
||||
|
||||
asset_store = _get_asset_store()
|
||||
video_path = (
|
||||
@@ -449,8 +449,8 @@ async def test_picture_source(
|
||||
|
||||
if isinstance(raw_stream, StaticImagePictureSource):
|
||||
# Static image stream: load image from asset
|
||||
from wled_controller.api.dependencies import get_asset_store as _get_asset_store
|
||||
from wled_controller.utils.image_codec import load_image_file
|
||||
from ledgrab.api.dependencies import get_asset_store as _get_asset_store
|
||||
from ledgrab.utils.image_codec import load_image_file
|
||||
|
||||
asset_store = _get_asset_store()
|
||||
image_path = (
|
||||
@@ -531,7 +531,7 @@ async def test_picture_source(
|
||||
image = last_frame.image
|
||||
|
||||
# Create thumbnail + encode (CPU-bound — run in thread)
|
||||
from wled_controller.utils.image_codec import (
|
||||
from ledgrab.utils.image_codec import (
|
||||
encode_jpeg_data_uri,
|
||||
thumbnail as make_thumbnail,
|
||||
)
|
||||
@@ -628,11 +628,11 @@ async def test_picture_source_ws(
|
||||
preview_width: int = Query(0),
|
||||
):
|
||||
"""WebSocket for picture source test with intermediate frame previews."""
|
||||
from wled_controller.api.routes._preview_helpers import (
|
||||
from ledgrab.api.routes._preview_helpers import (
|
||||
authenticate_ws_token,
|
||||
stream_capture_test,
|
||||
)
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.dependencies import (
|
||||
get_picture_source_store as _get_ps_store,
|
||||
get_template_store as _get_t_store,
|
||||
get_pp_template_store as _get_pp_store,
|
||||
@@ -662,8 +662,8 @@ async def test_picture_source_ws(
|
||||
|
||||
# Video sources: use VideoCaptureLiveStream for test preview
|
||||
if isinstance(raw_stream, VideoCaptureSource):
|
||||
from wled_controller.core.processing.video_stream import VideoCaptureLiveStream
|
||||
from wled_controller.api.dependencies import get_asset_store as _get_asset_store2
|
||||
from ledgrab.core.processing.video_stream import VideoCaptureLiveStream
|
||||
from ledgrab.api.dependencies import get_asset_store as _get_asset_store2
|
||||
|
||||
asset_store = _get_asset_store2()
|
||||
video_path = (
|
||||
@@ -690,7 +690,7 @@ async def test_picture_source_ws(
|
||||
|
||||
def _encode_video_frame(image, pw):
|
||||
"""Encode numpy RGB image as JPEG base64 data URI."""
|
||||
from wled_controller.utils.image_codec import encode_jpeg_data_uri, resize_down
|
||||
from ledgrab.utils.image_codec import encode_jpeg_data_uri, resize_down
|
||||
|
||||
if pw:
|
||||
image = resize_down(image, pw)
|
||||
+71
-30
@@ -5,34 +5,34 @@ import time
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_picture_source_store,
|
||||
get_pp_template_store,
|
||||
get_template_store,
|
||||
)
|
||||
from wled_controller.api.schemas.common import (
|
||||
from ledgrab.api.schemas.common import (
|
||||
CaptureImage,
|
||||
PerformanceMetrics,
|
||||
TemplateTestResponse,
|
||||
)
|
||||
from wled_controller.api.schemas.filters import FilterInstanceSchema
|
||||
from wled_controller.api.schemas.postprocessing import (
|
||||
from ledgrab.api.schemas.filters import FilterInstanceSchema
|
||||
from ledgrab.api.schemas.postprocessing import (
|
||||
PostprocessingTemplateCreate,
|
||||
PostprocessingTemplateListResponse,
|
||||
PostprocessingTemplateResponse,
|
||||
PostprocessingTemplateUpdate,
|
||||
PPTemplateTestRequest,
|
||||
)
|
||||
from wled_controller.core.capture_engines import EngineRegistry
|
||||
from wled_controller.core.filters import FilterRegistry, FilterInstance, ImagePool
|
||||
from wled_controller.storage.template_store import TemplateStore
|
||||
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
from wled_controller.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.core.capture_engines import EngineRegistry
|
||||
from ledgrab.core.filters import FilterRegistry, FilterInstance, ImagePool
|
||||
from ledgrab.storage.template_store import TemplateStore
|
||||
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
|
||||
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||
from ledgrab.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -52,7 +52,11 @@ def _pp_template_to_response(t) -> PostprocessingTemplateResponse:
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/postprocessing-templates", response_model=PostprocessingTemplateListResponse, tags=["Postprocessing Templates"])
|
||||
@router.get(
|
||||
"/api/v1/postprocessing-templates",
|
||||
response_model=PostprocessingTemplateListResponse,
|
||||
tags=["Postprocessing Templates"],
|
||||
)
|
||||
async def list_pp_templates(
|
||||
_auth: AuthRequired,
|
||||
store: PostprocessingTemplateStore = Depends(get_pp_template_store),
|
||||
@@ -63,7 +67,12 @@ async def list_pp_templates(
|
||||
return PostprocessingTemplateListResponse(templates=responses, count=len(responses))
|
||||
|
||||
|
||||
@router.post("/api/v1/postprocessing-templates", response_model=PostprocessingTemplateResponse, tags=["Postprocessing Templates"], status_code=201)
|
||||
@router.post(
|
||||
"/api/v1/postprocessing-templates",
|
||||
response_model=PostprocessingTemplateResponse,
|
||||
tags=["Postprocessing Templates"],
|
||||
status_code=201,
|
||||
)
|
||||
async def create_pp_template(
|
||||
data: PostprocessingTemplateCreate,
|
||||
_auth: AuthRequired,
|
||||
@@ -90,7 +99,11 @@ async def create_pp_template(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/api/v1/postprocessing-templates/{template_id}", response_model=PostprocessingTemplateResponse, tags=["Postprocessing Templates"])
|
||||
@router.get(
|
||||
"/api/v1/postprocessing-templates/{template_id}",
|
||||
response_model=PostprocessingTemplateResponse,
|
||||
tags=["Postprocessing Templates"],
|
||||
)
|
||||
async def get_pp_template(
|
||||
template_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -101,10 +114,16 @@ async def get_pp_template(
|
||||
template = store.get_template(template_id)
|
||||
return _pp_template_to_response(template)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail=f"Postprocessing template {template_id} not found")
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Postprocessing template {template_id} not found"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/v1/postprocessing-templates/{template_id}", response_model=PostprocessingTemplateResponse, tags=["Postprocessing Templates"])
|
||||
@router.put(
|
||||
"/api/v1/postprocessing-templates/{template_id}",
|
||||
response_model=PostprocessingTemplateResponse,
|
||||
tags=["Postprocessing Templates"],
|
||||
)
|
||||
async def update_pp_template(
|
||||
template_id: str,
|
||||
data: PostprocessingTemplateUpdate,
|
||||
@@ -113,7 +132,11 @@ async def update_pp_template(
|
||||
):
|
||||
"""Update a postprocessing template."""
|
||||
try:
|
||||
filters = [FilterInstance(f.filter_id, f.options) for f in data.filters] if data.filters is not None else None
|
||||
filters = (
|
||||
[FilterInstance(f.filter_id, f.options) for f in data.filters]
|
||||
if data.filters is not None
|
||||
else None
|
||||
)
|
||||
template = store.update_template(
|
||||
template_id=template_id,
|
||||
name=data.name,
|
||||
@@ -133,7 +156,11 @@ async def update_pp_template(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.delete("/api/v1/postprocessing-templates/{template_id}", status_code=204, tags=["Postprocessing Templates"])
|
||||
@router.delete(
|
||||
"/api/v1/postprocessing-templates/{template_id}",
|
||||
status_code=204,
|
||||
tags=["Postprocessing Templates"],
|
||||
)
|
||||
async def delete_pp_template(
|
||||
template_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -149,7 +176,7 @@ async def delete_pp_template(
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Cannot delete postprocessing template: it is referenced by picture source(s): {names}. "
|
||||
"Please reassign those streams before deleting.",
|
||||
"Please reassign those streams before deleting.",
|
||||
)
|
||||
store.delete_template(template_id)
|
||||
fire_entity_event("pp_template", "deleted", template_id)
|
||||
@@ -165,7 +192,11 @@ async def delete_pp_template(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.post("/api/v1/postprocessing-templates/{template_id}/test", response_model=TemplateTestResponse, tags=["Postprocessing Templates"])
|
||||
@router.post(
|
||||
"/api/v1/postprocessing-templates/{template_id}/test",
|
||||
response_model=TemplateTestResponse,
|
||||
tags=["Postprocessing Templates"],
|
||||
)
|
||||
async def test_pp_template(
|
||||
template_id: str,
|
||||
test_request: PPTemplateTestRequest,
|
||||
@@ -194,7 +225,7 @@ async def test_pp_template(
|
||||
|
||||
raw_stream = chain["raw_stream"]
|
||||
|
||||
from wled_controller.utils.image_codec import (
|
||||
from ledgrab.utils.image_codec import (
|
||||
encode_jpeg_data_uri,
|
||||
load_image_file,
|
||||
thumbnail as make_thumbnail,
|
||||
@@ -202,10 +233,14 @@ async def test_pp_template(
|
||||
|
||||
if isinstance(raw_stream, StaticImagePictureSource):
|
||||
# Static image: load from asset
|
||||
from wled_controller.api.dependencies import get_asset_store as _get_asset_store
|
||||
from ledgrab.api.dependencies import get_asset_store as _get_asset_store
|
||||
|
||||
asset_store = _get_asset_store()
|
||||
image_path = asset_store.get_file_path(raw_stream.image_asset_id) if raw_stream.image_asset_id else None
|
||||
image_path = (
|
||||
asset_store.get_file_path(raw_stream.image_asset_id)
|
||||
if raw_stream.image_asset_id
|
||||
else None
|
||||
)
|
||||
if not image_path:
|
||||
raise HTTPException(status_code=400, detail="Image asset not found or missing file")
|
||||
|
||||
@@ -238,7 +273,9 @@ async def test_pp_template(
|
||||
)
|
||||
stream.initialize()
|
||||
|
||||
logger.info(f"Starting {test_request.capture_duration}s PP template test for {template_id} using stream {test_request.source_stream_id}")
|
||||
logger.info(
|
||||
f"Starting {test_request.capture_duration}s PP template test for {template_id} using stream {test_request.source_stream_id}"
|
||||
)
|
||||
|
||||
frame_count = 0
|
||||
total_capture_time = 0.0
|
||||
@@ -346,11 +383,11 @@ async def test_pp_template_ws(
|
||||
preview_width: int = Query(0),
|
||||
):
|
||||
"""WebSocket for PP template test with intermediate frame previews."""
|
||||
from wled_controller.api.routes._preview_helpers import (
|
||||
from ledgrab.api.routes._preview_helpers import (
|
||||
authenticate_ws_token,
|
||||
stream_capture_test,
|
||||
)
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.dependencies import (
|
||||
get_picture_source_store as _get_ps_store,
|
||||
get_template_store as _get_t_store,
|
||||
get_pp_template_store as _get_pp_store,
|
||||
@@ -400,7 +437,9 @@ async def test_pp_template_ws(
|
||||
return
|
||||
|
||||
if capture_template.engine_type not in EngineRegistry.get_available_engines():
|
||||
await websocket.close(code=4003, reason=f"Engine '{capture_template.engine_type}' not available")
|
||||
await websocket.close(
|
||||
code=4003, reason=f"Engine '{capture_template.engine_type}' not available"
|
||||
)
|
||||
return
|
||||
|
||||
# Resolve PP filters
|
||||
@@ -422,7 +461,9 @@ async def test_pp_template_ws(
|
||||
|
||||
try:
|
||||
await stream_capture_test(
|
||||
websocket, engine_factory, duration,
|
||||
websocket,
|
||||
engine_factory,
|
||||
duration,
|
||||
pp_filters=pp_filters,
|
||||
preview_width=preview_width or None,
|
||||
)
|
||||
+26
-18
@@ -5,30 +5,30 @@ from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_output_target_store,
|
||||
get_processor_manager,
|
||||
get_scene_preset_store,
|
||||
)
|
||||
from wled_controller.api.schemas.scene_presets import (
|
||||
from ledgrab.api.schemas.scene_presets import (
|
||||
ActivateResponse,
|
||||
ScenePresetCreate,
|
||||
ScenePresetListResponse,
|
||||
ScenePresetResponse,
|
||||
ScenePresetUpdate,
|
||||
)
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.core.scenes.scene_activator import (
|
||||
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||
from ledgrab.core.scenes.scene_activator import (
|
||||
apply_scene_state,
|
||||
capture_current_snapshot,
|
||||
)
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.storage.scene_preset import ScenePreset
|
||||
from wled_controller.storage.scene_preset_store import ScenePresetStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||
from ledgrab.storage.scene_preset import ScenePreset
|
||||
from ledgrab.storage.scene_preset_store import ScenePresetStore
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -39,13 +39,16 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
|
||||
id=preset.id,
|
||||
name=preset.name,
|
||||
description=preset.description,
|
||||
targets=[{
|
||||
"target_id": t.target_id,
|
||||
"running": t.running,
|
||||
"color_strip_source_id": t.color_strip_source_id,
|
||||
"brightness_value_source_id": t.brightness_value_source_id,
|
||||
"fps": t.fps,
|
||||
} for t in preset.targets],
|
||||
targets=[
|
||||
{
|
||||
"target_id": t.target_id,
|
||||
"running": t.running,
|
||||
"color_strip_source_id": t.color_strip_source_id,
|
||||
"brightness_value_source_id": t.brightness_value_source_id,
|
||||
"fps": t.fps,
|
||||
}
|
||||
for t in preset.targets
|
||||
],
|
||||
order=preset.order,
|
||||
tags=preset.tags,
|
||||
created_at=preset.created_at,
|
||||
@@ -55,6 +58,7 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
|
||||
|
||||
# ===== CRUD =====
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/v1/scene-presets",
|
||||
response_model=ScenePresetResponse,
|
||||
@@ -180,7 +184,9 @@ async def update_scene_preset(
|
||||
tags=data.tags,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404 if "not found" in str(e).lower() else 400, detail=str(e))
|
||||
raise HTTPException(
|
||||
status_code=404 if "not found" in str(e).lower() else 400, detail=str(e)
|
||||
)
|
||||
fire_entity_event("scene_preset", "updated", preset_id)
|
||||
return _preset_to_response(preset)
|
||||
|
||||
@@ -206,6 +212,7 @@ async def delete_scene_preset(
|
||||
|
||||
# ===== Recapture =====
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/v1/scene-presets/{preset_id}/recapture",
|
||||
response_model=ScenePresetResponse,
|
||||
@@ -244,6 +251,7 @@ async def recapture_scene_preset(
|
||||
|
||||
# ===== Activate =====
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/v1/scene-presets/{preset_id}/activate",
|
||||
response_model=ActivateResponse,
|
||||
+29
-18
@@ -2,25 +2,25 @@
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_color_strip_store,
|
||||
get_sync_clock_manager,
|
||||
get_sync_clock_store,
|
||||
)
|
||||
from wled_controller.api.schemas.sync_clocks import (
|
||||
from ledgrab.api.schemas.sync_clocks import (
|
||||
SyncClockCreate,
|
||||
SyncClockListResponse,
|
||||
SyncClockResponse,
|
||||
SyncClockUpdate,
|
||||
)
|
||||
from wled_controller.storage.sync_clock import SyncClock
|
||||
from wled_controller.storage.sync_clock_store import SyncClockStore
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.core.processing.sync_clock_manager import SyncClockManager
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.sync_clock import SyncClock
|
||||
from ledgrab.storage.sync_clock_store import SyncClockStore
|
||||
from ledgrab.storage.color_strip_store import ColorStripStore
|
||||
from ledgrab.core.processing.sync_clock_manager import SyncClockManager
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -57,7 +57,9 @@ async def list_sync_clocks(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/v1/sync-clocks", response_model=SyncClockResponse, status_code=201, tags=["Sync Clocks"])
|
||||
@router.post(
|
||||
"/api/v1/sync-clocks", response_model=SyncClockResponse, status_code=201, tags=["Sync Clocks"]
|
||||
)
|
||||
async def create_sync_clock(
|
||||
data: SyncClockCreate,
|
||||
_auth: AuthRequired,
|
||||
@@ -81,7 +83,9 @@ async def create_sync_clock(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
||||
@router.get(
|
||||
"/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"]
|
||||
)
|
||||
async def get_sync_clock(
|
||||
clock_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -96,7 +100,9 @@ async def get_sync_clock(
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
||||
@router.put(
|
||||
"/api/v1/sync-clocks/{clock_id}", response_model=SyncClockResponse, tags=["Sync Clocks"]
|
||||
)
|
||||
async def update_sync_clock(
|
||||
clock_id: str,
|
||||
data: SyncClockUpdate,
|
||||
@@ -138,9 +144,7 @@ async def delete_sync_clock(
|
||||
# Check references
|
||||
for source in css_store.get_all_sources():
|
||||
if getattr(source, "clock_id", None) == clock_id:
|
||||
raise ValueError(
|
||||
f"Cannot delete: referenced by color strip source '{source.name}'"
|
||||
)
|
||||
raise ValueError(f"Cannot delete: referenced by color strip source '{source.name}'")
|
||||
manager.release_all_for(clock_id)
|
||||
store.delete_clock(clock_id)
|
||||
fire_entity_event("sync_clock", "deleted", clock_id)
|
||||
@@ -153,7 +157,10 @@ async def delete_sync_clock(
|
||||
|
||||
# ── Runtime control ──────────────────────────────────────────────────
|
||||
|
||||
@router.post("/api/v1/sync-clocks/{clock_id}/pause", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
||||
|
||||
@router.post(
|
||||
"/api/v1/sync-clocks/{clock_id}/pause", response_model=SyncClockResponse, tags=["Sync Clocks"]
|
||||
)
|
||||
async def pause_sync_clock(
|
||||
clock_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -170,7 +177,9 @@ async def pause_sync_clock(
|
||||
return _to_response(clock, manager)
|
||||
|
||||
|
||||
@router.post("/api/v1/sync-clocks/{clock_id}/resume", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
||||
@router.post(
|
||||
"/api/v1/sync-clocks/{clock_id}/resume", response_model=SyncClockResponse, tags=["Sync Clocks"]
|
||||
)
|
||||
async def resume_sync_clock(
|
||||
clock_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -187,7 +196,9 @@ async def resume_sync_clock(
|
||||
return _to_response(clock, manager)
|
||||
|
||||
|
||||
@router.post("/api/v1/sync-clocks/{clock_id}/reset", response_model=SyncClockResponse, tags=["Sync Clocks"])
|
||||
@router.post(
|
||||
"/api/v1/sync-clocks/{clock_id}/reset", response_model=SyncClockResponse, tags=["Sync Clocks"]
|
||||
)
|
||||
async def reset_sync_clock(
|
||||
clock_id: str,
|
||||
_auth: AuthRequired,
|
||||
+14
-14
@@ -15,9 +15,9 @@ import os
|
||||
import psutil
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from wled_controller import __version__, REPO_URL, DONATE_URL
|
||||
from wled_controller.api.auth import AuthRequired, is_auth_enabled
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab import __version__, REPO_URL, DONATE_URL
|
||||
from ledgrab.api.auth import AuthRequired, is_auth_enabled
|
||||
from ledgrab.api.dependencies import (
|
||||
get_audio_source_store,
|
||||
get_audio_template_store,
|
||||
get_automation_store,
|
||||
@@ -34,7 +34,7 @@ from wled_controller.api.dependencies import (
|
||||
get_template_store,
|
||||
get_value_source_store,
|
||||
)
|
||||
from wled_controller.api.schemas.system import (
|
||||
from ledgrab.api.schemas.system import (
|
||||
DisplayInfo,
|
||||
DisplayListResponse,
|
||||
GpuInfo,
|
||||
@@ -43,13 +43,13 @@ from wled_controller.api.schemas.system import (
|
||||
ProcessListResponse,
|
||||
VersionResponse,
|
||||
)
|
||||
from wled_controller.config import get_config, is_demo_mode
|
||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.config import get_config, is_demo_mode
|
||||
from ledgrab.core.capture.screen_capture import get_available_displays
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
# Re-export load_external_url so existing callers still work
|
||||
from wled_controller.api.routes.system_settings import load_external_url # noqa: F401
|
||||
from ledgrab.api.routes.system_settings import load_external_url # noqa: F401
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -59,7 +59,7 @@ _process = psutil.Process(os.getpid())
|
||||
_process.cpu_percent(interval=None) # prime process-level counter
|
||||
|
||||
# GPU monitoring (initialized once in utils.gpu, shared with metrics_history)
|
||||
from wled_controller.utils.gpu import ( # noqa: E402
|
||||
from ledgrab.utils.gpu import ( # noqa: E402
|
||||
nvml_available as _nvml_available,
|
||||
nvml as _nvml,
|
||||
nvml_handle as _nvml_handle,
|
||||
@@ -139,7 +139,7 @@ async def get_version():
|
||||
async def list_all_tags(_: AuthRequired):
|
||||
"""Get all tags used across all entities."""
|
||||
all_tags: set[str] = set()
|
||||
from wled_controller.api.dependencies import get_asset_store
|
||||
from ledgrab.api.dependencies import get_asset_store
|
||||
|
||||
store_getters = [
|
||||
get_device_store,
|
||||
@@ -185,7 +185,7 @@ async def get_displays(
|
||||
logger.info(f"Listing available displays (engine_type={engine_type})")
|
||||
|
||||
try:
|
||||
from wled_controller.core.capture_engines import EngineRegistry
|
||||
from ledgrab.core.capture_engines import EngineRegistry
|
||||
|
||||
if engine_type:
|
||||
engine_cls = EngineRegistry.get_engine(engine_type)
|
||||
@@ -240,7 +240,7 @@ async def get_running_processes(_: AuthRequired):
|
||||
|
||||
Returns a sorted list of unique process names for use in automation conditions.
|
||||
"""
|
||||
from wled_controller.core.automations.platform_detector import PlatformDetector
|
||||
from ledgrab.core.automations.platform_detector import PlatformDetector
|
||||
|
||||
try:
|
||||
detector = PlatformDetector()
|
||||
@@ -348,7 +348,7 @@ async def get_integrations_status(
|
||||
|
||||
Used by the dashboard to show connectivity indicators.
|
||||
"""
|
||||
from wled_controller.core.devices.mqtt_client import get_mqtt_service
|
||||
from ledgrab.core.devices.mqtt_client import get_mqtt_service
|
||||
|
||||
# MQTT status
|
||||
mqtt_service = get_mqtt_service()
|
||||
+26
-17
@@ -10,9 +10,9 @@ import re
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
from pydantic import BaseModel
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import get_database
|
||||
from wled_controller.api.schemas.system import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import get_database
|
||||
from ledgrab.api.schemas.system import (
|
||||
ExternalUrlRequest,
|
||||
ExternalUrlResponse,
|
||||
LogLevelRequest,
|
||||
@@ -20,9 +20,9 @@ from wled_controller.api.schemas.system import (
|
||||
MQTTSettingsRequest,
|
||||
MQTTSettingsResponse,
|
||||
)
|
||||
from wled_controller.config import get_config
|
||||
from wled_controller.storage.database import Database
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.config import get_config
|
||||
from ledgrab.storage.database import Database
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -76,7 +76,9 @@ async def get_mqtt_settings(_: AuthRequired, db: Database = Depends(get_database
|
||||
response_model=MQTTSettingsResponse,
|
||||
tags=["System"],
|
||||
)
|
||||
async def update_mqtt_settings(_: AuthRequired, body: MQTTSettingsRequest, db: Database = Depends(get_database)):
|
||||
async def update_mqtt_settings(
|
||||
_: AuthRequired, body: MQTTSettingsRequest, db: Database = Depends(get_database)
|
||||
):
|
||||
"""Update MQTT broker settings. If password is empty string, the existing password is preserved."""
|
||||
current = _load_mqtt_settings(db)
|
||||
|
||||
@@ -110,10 +112,12 @@ async def update_mqtt_settings(_: AuthRequired, body: MQTTSettingsRequest, db: D
|
||||
# External URL setting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_external_url(db: Database | None = None) -> str:
|
||||
"""Load the external URL setting. Returns empty string if not set."""
|
||||
if db is None:
|
||||
from wled_controller.api.dependencies import get_database
|
||||
from ledgrab.api.dependencies import get_database
|
||||
|
||||
db = get_database()
|
||||
data = db.get_setting("external_url")
|
||||
if data:
|
||||
@@ -136,7 +140,9 @@ async def get_external_url(_: AuthRequired, db: Database = Depends(get_database)
|
||||
response_model=ExternalUrlResponse,
|
||||
tags=["System"],
|
||||
)
|
||||
async def update_external_url(_: AuthRequired, body: ExternalUrlRequest, db: Database = Depends(get_database)):
|
||||
async def update_external_url(
|
||||
_: AuthRequired, body: ExternalUrlRequest, db: Database = Depends(get_database)
|
||||
):
|
||||
"""Set the external base URL used in webhook URLs and other user-visible URLs."""
|
||||
url = body.external_url.strip().rstrip("/")
|
||||
db.set_setting("external_url", {"external_url": url})
|
||||
@@ -159,8 +165,8 @@ async def logs_ws(
|
||||
Auth via ``?token=<api_key>``. On connect, sends the last ~500 buffered
|
||||
lines as individual text messages, then pushes new lines as they appear.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from wled_controller.utils import log_broadcaster
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
from ledgrab.utils import log_broadcaster
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
@@ -205,9 +211,7 @@ async def logs_ws(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Regex: IPv4 address with optional port, e.g. "192.168.1.5" or "192.168.1.5:5555"
|
||||
_ADB_ADDRESS_RE = re.compile(
|
||||
r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?$"
|
||||
)
|
||||
_ADB_ADDRESS_RE = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?$")
|
||||
|
||||
|
||||
class AdbConnectRequest(BaseModel):
|
||||
@@ -244,7 +248,8 @@ def _validate_adb_address(address: str) -> None:
|
||||
|
||||
def _get_adb_path() -> str:
|
||||
"""Get the adb binary path from the scrcpy engine's resolver."""
|
||||
from wled_controller.core.capture_engines.scrcpy_engine import _get_adb
|
||||
from ledgrab.core.capture_engines.scrcpy_engine import _get_adb
|
||||
|
||||
return _get_adb()
|
||||
|
||||
|
||||
@@ -265,7 +270,9 @@ async def adb_connect(_: AuthRequired, request: AdbConnectRequest):
|
||||
logger.info(f"Connecting ADB device: {address}")
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
adb, "connect", address,
|
||||
adb,
|
||||
"connect",
|
||||
address,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
@@ -295,7 +302,9 @@ async def adb_disconnect(_: AuthRequired, request: AdbConnectRequest):
|
||||
logger.info(f"Disconnecting ADB device: {address}")
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
adb, "disconnect", address,
|
||||
adb,
|
||||
"disconnect",
|
||||
address,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
+95
-60
@@ -5,20 +5,20 @@ import time
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_cspt_store,
|
||||
get_picture_source_store,
|
||||
get_pp_template_store,
|
||||
get_template_store,
|
||||
)
|
||||
from wled_controller.api.schemas.common import (
|
||||
from ledgrab.api.schemas.common import (
|
||||
CaptureImage,
|
||||
PerformanceMetrics,
|
||||
TemplateTestResponse,
|
||||
)
|
||||
from wled_controller.api.schemas.templates import (
|
||||
from ledgrab.api.schemas.templates import (
|
||||
EngineInfo,
|
||||
EngineListResponse,
|
||||
TemplateCreate,
|
||||
@@ -27,18 +27,18 @@ from wled_controller.api.schemas.templates import (
|
||||
TemplateTestRequest,
|
||||
TemplateUpdate,
|
||||
)
|
||||
from wled_controller.api.schemas.filters import (
|
||||
from ledgrab.api.schemas.filters import (
|
||||
FilterOptionDefSchema,
|
||||
FilterTypeListResponse,
|
||||
FilterTypeResponse,
|
||||
)
|
||||
from wled_controller.core.capture_engines import EngineRegistry
|
||||
from wled_controller.core.filters import FilterRegistry
|
||||
from wled_controller.storage.template_store import TemplateStore
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
from wled_controller.storage.picture_source import ScreenCapturePictureSource
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.core.capture_engines import EngineRegistry
|
||||
from ledgrab.core.filters import FilterRegistry
|
||||
from ledgrab.storage.template_store import TemplateStore
|
||||
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||
from ledgrab.storage.picture_source import ScreenCapturePictureSource
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -47,6 +47,7 @@ router = APIRouter()
|
||||
|
||||
# ===== CAPTURE TEMPLATE ENDPOINTS =====
|
||||
|
||||
|
||||
@router.get("/api/v1/capture-templates", response_model=TemplateListResponse, tags=["Templates"])
|
||||
async def list_templates(
|
||||
_auth: AuthRequired,
|
||||
@@ -80,7 +81,12 @@ async def list_templates(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.post("/api/v1/capture-templates", response_model=TemplateResponse, tags=["Templates"], status_code=201)
|
||||
@router.post(
|
||||
"/api/v1/capture-templates",
|
||||
response_model=TemplateResponse,
|
||||
tags=["Templates"],
|
||||
status_code=201,
|
||||
)
|
||||
async def create_template(
|
||||
template_data: TemplateCreate,
|
||||
_auth: AuthRequired,
|
||||
@@ -111,7 +117,6 @@ async def create_template(
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
@@ -119,7 +124,9 @@ async def create_template(
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/api/v1/capture-templates/{template_id}", response_model=TemplateResponse, tags=["Templates"])
|
||||
@router.get(
|
||||
"/api/v1/capture-templates/{template_id}", response_model=TemplateResponse, tags=["Templates"]
|
||||
)
|
||||
async def get_template(
|
||||
template_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -143,7 +150,9 @@ async def get_template(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/v1/capture-templates/{template_id}", response_model=TemplateResponse, tags=["Templates"])
|
||||
@router.put(
|
||||
"/api/v1/capture-templates/{template_id}", response_model=TemplateResponse, tags=["Templates"]
|
||||
)
|
||||
async def update_template(
|
||||
template_id: str,
|
||||
update_data: TemplateUpdate,
|
||||
@@ -176,7 +185,6 @@ async def update_template(
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
@@ -199,7 +207,10 @@ async def delete_template(
|
||||
# Check if any streams are using this template
|
||||
streams_using_template = []
|
||||
for stream in stream_store.get_all_streams():
|
||||
if isinstance(stream, ScreenCapturePictureSource) and stream.capture_template_id == template_id:
|
||||
if (
|
||||
isinstance(stream, ScreenCapturePictureSource)
|
||||
and stream.capture_template_id == template_id
|
||||
):
|
||||
streams_using_template.append(stream.name)
|
||||
|
||||
if streams_using_template:
|
||||
@@ -207,7 +218,7 @@ async def delete_template(
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Cannot delete template: it is used by the following stream(s): {stream_list}. "
|
||||
f"Please reassign these streams to a different template before deleting."
|
||||
f"Please reassign these streams to a different template before deleting.",
|
||||
)
|
||||
|
||||
# Proceed with deletion
|
||||
@@ -245,7 +256,7 @@ async def list_engines(_auth: AuthRequired):
|
||||
name=engine_type.upper(),
|
||||
default_config=engine_class.get_default_config(),
|
||||
available=(engine_type in available_set),
|
||||
has_own_displays=getattr(engine_class, 'HAS_OWN_DISPLAYS', False),
|
||||
has_own_displays=getattr(engine_class, "HAS_OWN_DISPLAYS", False),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -256,7 +267,9 @@ async def list_engines(_auth: AuthRequired):
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.post("/api/v1/capture-templates/test", response_model=TemplateTestResponse, tags=["Templates"])
|
||||
@router.post(
|
||||
"/api/v1/capture-templates/test", response_model=TemplateTestResponse, tags=["Templates"]
|
||||
)
|
||||
def test_template(
|
||||
test_request: TemplateTestRequest,
|
||||
_auth: AuthRequired,
|
||||
@@ -276,7 +289,7 @@ def test_template(
|
||||
if test_request.engine_type not in EngineRegistry.get_available_engines():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Engine '{test_request.engine_type}' is not available on this system"
|
||||
detail=f"Engine '{test_request.engine_type}' is not available on this system",
|
||||
)
|
||||
|
||||
# Create and initialize capture stream
|
||||
@@ -286,7 +299,9 @@ def test_template(
|
||||
stream.initialize()
|
||||
|
||||
# Run sustained capture test
|
||||
logger.info(f"Starting {test_request.capture_duration}s capture test with {test_request.engine_type}")
|
||||
logger.info(
|
||||
f"Starting {test_request.capture_duration}s capture test with {test_request.engine_type}"
|
||||
)
|
||||
|
||||
frame_count = 0
|
||||
total_capture_time = 0.0
|
||||
@@ -321,7 +336,7 @@ def test_template(
|
||||
raise ValueError("Unexpected image format from engine")
|
||||
image = last_frame.image
|
||||
|
||||
from wled_controller.utils.image_codec import (
|
||||
from ledgrab.utils.image_codec import (
|
||||
encode_jpeg_data_uri,
|
||||
thumbnail as make_thumbnail,
|
||||
)
|
||||
@@ -361,7 +376,6 @@ def test_template(
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except RuntimeError as e:
|
||||
@@ -391,7 +405,7 @@ async def test_template_ws(
|
||||
Config is sent as the first client message (JSON with engine_type,
|
||||
engine_config, display_index, capture_duration).
|
||||
"""
|
||||
from wled_controller.api.routes._preview_helpers import (
|
||||
from ledgrab.api.routes._preview_helpers import (
|
||||
authenticate_ws_token,
|
||||
stream_capture_test,
|
||||
)
|
||||
@@ -417,7 +431,9 @@ async def test_template_ws(
|
||||
pw = int(config.get("preview_width", 0)) or None
|
||||
|
||||
if engine_type not in EngineRegistry.get_available_engines():
|
||||
await websocket.send_json({"type": "error", "detail": f"Engine '{engine_type}' not available"})
|
||||
await websocket.send_json(
|
||||
{"type": "error", "detail": f"Engine '{engine_type}' not available"}
|
||||
)
|
||||
await websocket.close(code=4003)
|
||||
return
|
||||
|
||||
@@ -428,7 +444,9 @@ async def test_template_ws(
|
||||
s.initialize()
|
||||
return s
|
||||
|
||||
logger.info(f"Capture template test WS connected ({engine_type}, display {display_index}, {duration}s)")
|
||||
logger.info(
|
||||
f"Capture template test WS connected ({engine_type}, display {display_index}, {duration}s)"
|
||||
)
|
||||
|
||||
try:
|
||||
await stream_capture_test(websocket, engine_factory, duration, preview_width=pw)
|
||||
@@ -443,6 +461,7 @@ async def test_template_ws(
|
||||
|
||||
# ===== FILTER TYPE ENDPOINTS =====
|
||||
|
||||
|
||||
@router.get("/api/v1/filters", response_model=FilterTypeListResponse, tags=["Filters"])
|
||||
async def list_filter_types(
|
||||
_auth: AuthRequired,
|
||||
@@ -467,23 +486,31 @@ async def list_filter_types(
|
||||
for opt in schema:
|
||||
choices = opt.choices
|
||||
# Enrich filter_template choices with current template list
|
||||
if filter_id == "filter_template" and opt.key == "template_id" and template_choices is not None:
|
||||
if (
|
||||
filter_id == "filter_template"
|
||||
and opt.key == "template_id"
|
||||
and template_choices is not None
|
||||
):
|
||||
choices = template_choices
|
||||
opt_schemas.append(FilterOptionDefSchema(
|
||||
key=opt.key,
|
||||
label=opt.label,
|
||||
type=opt.option_type,
|
||||
default=opt.default,
|
||||
min_value=opt.min_value,
|
||||
max_value=opt.max_value,
|
||||
step=opt.step,
|
||||
choices=choices,
|
||||
))
|
||||
responses.append(FilterTypeResponse(
|
||||
filter_id=filter_cls.filter_id,
|
||||
filter_name=filter_cls.filter_name,
|
||||
options_schema=opt_schemas,
|
||||
))
|
||||
opt_schemas.append(
|
||||
FilterOptionDefSchema(
|
||||
key=opt.key,
|
||||
label=opt.label,
|
||||
type=opt.option_type,
|
||||
default=opt.default,
|
||||
min_value=opt.min_value,
|
||||
max_value=opt.max_value,
|
||||
step=opt.step,
|
||||
choices=choices,
|
||||
)
|
||||
)
|
||||
responses.append(
|
||||
FilterTypeResponse(
|
||||
filter_id=filter_cls.filter_id,
|
||||
filter_name=filter_cls.filter_name,
|
||||
options_schema=opt_schemas,
|
||||
)
|
||||
)
|
||||
return FilterTypeListResponse(filters=responses, count=len(responses))
|
||||
|
||||
|
||||
@@ -512,21 +539,29 @@ async def list_strip_filter_types(
|
||||
opt_schemas = []
|
||||
for opt in schema:
|
||||
choices = opt.choices
|
||||
if filter_id == "css_filter_template" and opt.key == "template_id" and cspt_choices is not None:
|
||||
if (
|
||||
filter_id == "css_filter_template"
|
||||
and opt.key == "template_id"
|
||||
and cspt_choices is not None
|
||||
):
|
||||
choices = cspt_choices
|
||||
opt_schemas.append(FilterOptionDefSchema(
|
||||
key=opt.key,
|
||||
label=opt.label,
|
||||
type=opt.option_type,
|
||||
default=opt.default,
|
||||
min_value=opt.min_value,
|
||||
max_value=opt.max_value,
|
||||
step=opt.step,
|
||||
choices=choices,
|
||||
))
|
||||
responses.append(FilterTypeResponse(
|
||||
filter_id=filter_cls.filter_id,
|
||||
filter_name=filter_cls.filter_name,
|
||||
options_schema=opt_schemas,
|
||||
))
|
||||
opt_schemas.append(
|
||||
FilterOptionDefSchema(
|
||||
key=opt.key,
|
||||
label=opt.label,
|
||||
type=opt.option_type,
|
||||
default=opt.default,
|
||||
min_value=opt.min_value,
|
||||
max_value=opt.max_value,
|
||||
step=opt.step,
|
||||
choices=choices,
|
||||
)
|
||||
)
|
||||
responses.append(
|
||||
FilterTypeResponse(
|
||||
filter_id=filter_cls.filter_id,
|
||||
filter_name=filter_cls.filter_name,
|
||||
options_schema=opt_schemas,
|
||||
)
|
||||
)
|
||||
return FilterTypeListResponse(filters=responses, count=len(responses))
|
||||
+8
-6
@@ -3,16 +3,16 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import get_update_service
|
||||
from wled_controller.api.schemas.update import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import get_update_service
|
||||
from ledgrab.api.schemas.update import (
|
||||
DismissRequest,
|
||||
UpdateSettingsRequest,
|
||||
UpdateSettingsResponse,
|
||||
UpdateStatusResponse,
|
||||
)
|
||||
from wled_controller.core.update.update_service import UpdateService
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.update.update_service import UpdateService
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -57,7 +57,9 @@ async def apply_update(
|
||||
if not status["can_auto_update"]:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"detail": f"Auto-update not supported for install type: {status['install_type']}"},
|
||||
content={
|
||||
"detail": f"Auto-update not supported for install type: {status['install_type']}"
|
||||
},
|
||||
)
|
||||
try:
|
||||
await service.apply_update()
|
||||
+12
-12
@@ -5,14 +5,14 @@ from typing import Annotated, Optional
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_output_target_store,
|
||||
get_processor_manager,
|
||||
get_value_source_store,
|
||||
)
|
||||
from wled_controller.api.schemas.value_sources import (
|
||||
from ledgrab.api.schemas.value_sources import (
|
||||
AdaptiveSceneValueSourceResponse,
|
||||
AdaptiveTimeColorValueSourceResponse,
|
||||
AdaptiveTimeValueSourceResponse,
|
||||
@@ -31,7 +31,7 @@ from wled_controller.api.schemas.value_sources import (
|
||||
ValueSourceResponse,
|
||||
ValueSourceUpdate,
|
||||
)
|
||||
from wled_controller.storage.value_source import (
|
||||
from ledgrab.storage.value_source import (
|
||||
AdaptiveTimeColorValueSource,
|
||||
AdaptiveValueSource,
|
||||
AnimatedColorValueSource,
|
||||
@@ -46,12 +46,12 @@ from wled_controller.storage.value_source import (
|
||||
SystemMetricsValueSource,
|
||||
ValueSource,
|
||||
)
|
||||
from wled_controller.storage.value_source_store import ValueSourceStore
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.core.processing.value_stream import ValueStream
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.value_source_store import ValueSourceStore
|
||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||
from ledgrab.core.processing.processor_manager import ProcessorManager
|
||||
from ledgrab.core.processing.value_stream import ValueStream
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -340,7 +340,7 @@ async def delete_value_source(
|
||||
"""Delete a value source."""
|
||||
try:
|
||||
# Check if any targets reference this value source
|
||||
from wled_controller.storage.wled_output_target import WledOutputTarget
|
||||
from ledgrab.storage.wled_output_target import WledOutputTarget
|
||||
|
||||
for target in target_store.get_all_targets():
|
||||
if isinstance(target, WledOutputTarget):
|
||||
@@ -370,7 +370,7 @@ async def test_value_source_ws(
|
||||
Acquires a ValueStream for the given source, polls get_value() at ~20 Hz,
|
||||
and streams {value: float} JSON to the client.
|
||||
"""
|
||||
from wled_controller.api.auth import verify_ws_token
|
||||
from ledgrab.api.auth import verify_ws_token
|
||||
|
||||
if not verify_ws_token(token):
|
||||
await websocket.close(code=4001, reason="Unauthorized")
|
||||
+33
-14
@@ -2,25 +2,25 @@
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
from ledgrab.api.auth import AuthRequired
|
||||
from ledgrab.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_weather_manager,
|
||||
get_weather_source_store,
|
||||
)
|
||||
from wled_controller.api.schemas.weather_sources import (
|
||||
from ledgrab.api.schemas.weather_sources import (
|
||||
WeatherSourceCreate,
|
||||
WeatherSourceListResponse,
|
||||
WeatherSourceResponse,
|
||||
WeatherSourceUpdate,
|
||||
WeatherTestResponse,
|
||||
)
|
||||
from wled_controller.core.weather.weather_manager import WeatherManager
|
||||
from wled_controller.core.weather.weather_provider import WMO_CONDITION_NAMES
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from wled_controller.storage.weather_source import WeatherSource
|
||||
from wled_controller.storage.weather_source_store import WeatherSourceStore
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.weather.weather_manager import WeatherManager
|
||||
from ledgrab.core.weather.weather_provider import WMO_CONDITION_NAMES
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
from ledgrab.storage.weather_source import WeatherSource
|
||||
from ledgrab.storage.weather_source_store import WeatherSourceStore
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -44,7 +44,9 @@ def _to_response(source: WeatherSource) -> WeatherSourceResponse:
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/weather-sources", response_model=WeatherSourceListResponse, tags=["Weather Sources"])
|
||||
@router.get(
|
||||
"/api/v1/weather-sources", response_model=WeatherSourceListResponse, tags=["Weather Sources"]
|
||||
)
|
||||
async def list_weather_sources(
|
||||
_auth: AuthRequired,
|
||||
store: WeatherSourceStore = Depends(get_weather_source_store),
|
||||
@@ -56,7 +58,12 @@ async def list_weather_sources(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/v1/weather-sources", response_model=WeatherSourceResponse, status_code=201, tags=["Weather Sources"])
|
||||
@router.post(
|
||||
"/api/v1/weather-sources",
|
||||
response_model=WeatherSourceResponse,
|
||||
status_code=201,
|
||||
tags=["Weather Sources"],
|
||||
)
|
||||
async def create_weather_source(
|
||||
data: WeatherSourceCreate,
|
||||
_auth: AuthRequired,
|
||||
@@ -79,7 +86,11 @@ async def create_weather_source(
|
||||
return _to_response(source)
|
||||
|
||||
|
||||
@router.get("/api/v1/weather-sources/{source_id}", response_model=WeatherSourceResponse, tags=["Weather Sources"])
|
||||
@router.get(
|
||||
"/api/v1/weather-sources/{source_id}",
|
||||
response_model=WeatherSourceResponse,
|
||||
tags=["Weather Sources"],
|
||||
)
|
||||
async def get_weather_source(
|
||||
source_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -91,7 +102,11 @@ async def get_weather_source(
|
||||
raise HTTPException(status_code=404, detail=f"Weather source {source_id} not found")
|
||||
|
||||
|
||||
@router.put("/api/v1/weather-sources/{source_id}", response_model=WeatherSourceResponse, tags=["Weather Sources"])
|
||||
@router.put(
|
||||
"/api/v1/weather-sources/{source_id}",
|
||||
response_model=WeatherSourceResponse,
|
||||
tags=["Weather Sources"],
|
||||
)
|
||||
async def update_weather_source(
|
||||
source_id: str,
|
||||
data: WeatherSourceUpdate,
|
||||
@@ -133,7 +148,11 @@ async def delete_weather_source(
|
||||
fire_entity_event("weather_source", "deleted", source_id)
|
||||
|
||||
|
||||
@router.post("/api/v1/weather-sources/{source_id}/test", response_model=WeatherTestResponse, tags=["Weather Sources"])
|
||||
@router.post(
|
||||
"/api/v1/weather-sources/{source_id}/test",
|
||||
response_model=WeatherTestResponse,
|
||||
tags=["Weather Sources"],
|
||||
)
|
||||
async def test_weather_source(
|
||||
source_id: str,
|
||||
_auth: AuthRequired,
|
||||
+12
-7
@@ -13,11 +13,11 @@ from collections import defaultdict
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from wled_controller.api.dependencies import get_automation_engine, get_automation_store
|
||||
from wled_controller.core.automations.automation_engine import AutomationEngine
|
||||
from wled_controller.storage.automation import WebhookCondition
|
||||
from wled_controller.storage.automation_store import AutomationStore
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.api.dependencies import get_automation_engine, get_automation_store
|
||||
from ledgrab.core.automations.automation_engine import AutomationEngine
|
||||
from ledgrab.storage.automation import WebhookCondition
|
||||
from ledgrab.storage.automation_store import AutomationStore
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -75,12 +75,17 @@ async def handle_webhook(
|
||||
# Find the automation that owns this token
|
||||
for automation in store.get_all_automations():
|
||||
for condition in automation.conditions:
|
||||
if isinstance(condition, WebhookCondition) and secrets.compare_digest(condition.token, token):
|
||||
if isinstance(condition, WebhookCondition) and secrets.compare_digest(
|
||||
condition.token, token
|
||||
):
|
||||
active = body.action == "activate"
|
||||
await engine.set_webhook_state(token, active)
|
||||
logger.info(
|
||||
"Webhook %s: automation '%s' (%s) → %s",
|
||||
token[:8], automation.name, automation.id, body.action,
|
||||
token[:8],
|
||||
automation.name,
|
||||
automation.id,
|
||||
body.action,
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
+3
-1
@@ -10,7 +10,9 @@ class AudioTemplateCreate(BaseModel):
|
||||
"""Request to create an audio capture template."""
|
||||
|
||||
name: str = Field(description="Template name", min_length=1, max_length=100)
|
||||
engine_type: str = Field(description="Audio engine type (e.g., 'wasapi', 'sounddevice')", min_length=1)
|
||||
engine_type: str = Field(
|
||||
description="Audio engine type (e.g., 'wasapi', 'sounddevice')", min_length=1
|
||||
)
|
||||
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
+6
-2
@@ -12,7 +12,9 @@ class ColorStripProcessingTemplateCreate(BaseModel):
|
||||
"""Request to create a color strip processing template."""
|
||||
|
||||
name: str = Field(description="Template name", min_length=1, max_length=100)
|
||||
filters: List[FilterInstanceSchema] = Field(default_factory=list, description="Ordered list of filter instances")
|
||||
filters: List[FilterInstanceSchema] = Field(
|
||||
default_factory=list, description="Ordered list of filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
|
||||
@@ -21,7 +23,9 @@ class ColorStripProcessingTemplateUpdate(BaseModel):
|
||||
"""Request to update a color strip processing template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
filters: Optional[List[FilterInstanceSchema]] = Field(None, description="Ordered list of filter instances")
|
||||
filters: Optional[List[FilterInstanceSchema]] = Field(
|
||||
None, description="Ordered list of filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Discriminator, Field, Tag, model_validator
|
||||
|
||||
from wled_controller.api.schemas.devices import Calibration
|
||||
from ledgrab.api.schemas.devices import Calibration
|
||||
|
||||
|
||||
# =====================================================================
|
||||
+3
-1
@@ -48,5 +48,7 @@ class TemplateTestResponse(BaseModel):
|
||||
"""Response from template test."""
|
||||
|
||||
full_capture: CaptureImage = Field(description="Full screen capture with thumbnail")
|
||||
border_extraction: Optional[BorderExtraction] = Field(None, description="Extracted border images (deprecated)")
|
||||
border_extraction: Optional[BorderExtraction] = Field(
|
||||
None, description="Extracted border images (deprecated)"
|
||||
)
|
||||
performance: PerformanceMetrics = Field(description="Performance metrics")
|
||||
+6
-2
@@ -22,8 +22,12 @@ class FilterOptionDefSchema(BaseModel):
|
||||
min_value: Any = Field(description="Minimum value")
|
||||
max_value: Any = Field(description="Maximum value")
|
||||
step: Any = Field(description="Step increment")
|
||||
choices: Optional[List[Dict[str, str]]] = Field(default=None, description="Available choices for select type")
|
||||
max_length: Optional[int] = Field(default=None, description="Maximum string length for string type")
|
||||
choices: Optional[List[Dict[str, str]]] = Field(
|
||||
default=None, description="Available choices for select type"
|
||||
)
|
||||
max_length: Optional[int] = Field(
|
||||
default=None, description="Maximum string length for string type"
|
||||
)
|
||||
|
||||
|
||||
class FilterTypeResponse(BaseModel):
|
||||
+6
-2
@@ -12,7 +12,9 @@ class PatternTemplateCreate(BaseModel):
|
||||
"""Request to create a pattern template."""
|
||||
|
||||
name: str = Field(description="Template name", min_length=1, max_length=100)
|
||||
rectangles: List[KeyColorRectangleSchema] = Field(default_factory=list, description="List of named rectangles")
|
||||
rectangles: List[KeyColorRectangleSchema] = Field(
|
||||
default_factory=list, description="List of named rectangles"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
|
||||
@@ -21,7 +23,9 @@ class PatternTemplateUpdate(BaseModel):
|
||||
"""Request to update a pattern template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
rectangles: Optional[List[KeyColorRectangleSchema]] = Field(None, description="List of named rectangles")
|
||||
rectangles: Optional[List[KeyColorRectangleSchema]] = Field(
|
||||
None, description="List of named rectangles"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
+12
-4
@@ -12,7 +12,9 @@ class PostprocessingTemplateCreate(BaseModel):
|
||||
"""Request to create a postprocessing template."""
|
||||
|
||||
name: str = Field(description="Template name", min_length=1, max_length=100)
|
||||
filters: List[FilterInstanceSchema] = Field(default_factory=list, description="Ordered list of filter instances")
|
||||
filters: List[FilterInstanceSchema] = Field(
|
||||
default_factory=list, description="Ordered list of filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
|
||||
@@ -21,7 +23,9 @@ class PostprocessingTemplateUpdate(BaseModel):
|
||||
"""Request to update a postprocessing template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
filters: Optional[List[FilterInstanceSchema]] = Field(None, description="Ordered list of filter instances")
|
||||
filters: Optional[List[FilterInstanceSchema]] = Field(
|
||||
None, description="Ordered list of filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
@@ -41,7 +45,9 @@ class PostprocessingTemplateResponse(BaseModel):
|
||||
class PostprocessingTemplateListResponse(BaseModel):
|
||||
"""List of postprocessing templates response."""
|
||||
|
||||
templates: List[PostprocessingTemplateResponse] = Field(description="List of postprocessing templates")
|
||||
templates: List[PostprocessingTemplateResponse] = Field(
|
||||
description="List of postprocessing templates"
|
||||
)
|
||||
count: int = Field(description="Number of templates")
|
||||
|
||||
|
||||
@@ -49,4 +55,6 @@ class PPTemplateTestRequest(BaseModel):
|
||||
"""Request to test a postprocessing template against a source stream."""
|
||||
|
||||
source_stream_id: str = Field(description="ID of the source picture source to capture from")
|
||||
capture_duration: float = Field(default=5.0, ge=1.0, le=30.0, description="Duration to capture in seconds")
|
||||
capture_duration: float = Field(
|
||||
default=5.0, ge=1.0, le=30.0, description="Duration to capture in seconds"
|
||||
)
|
||||
+7
-2
@@ -19,7 +19,9 @@ class ScenePresetCreate(BaseModel):
|
||||
|
||||
name: str = Field(description="Preset name", min_length=1, max_length=100)
|
||||
description: str = Field(default="", max_length=500)
|
||||
target_ids: Optional[List[str]] = Field(None, description="Target IDs to capture (all if omitted)")
|
||||
target_ids: Optional[List[str]] = Field(
|
||||
None, description="Target IDs to capture (all if omitted)"
|
||||
)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
|
||||
|
||||
@@ -29,7 +31,10 @@ class ScenePresetUpdate(BaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
order: Optional[int] = None
|
||||
target_ids: Optional[List[str]] = Field(None, description="Update target list: keep state for existing, capture fresh for new, drop removed")
|
||||
target_ids: Optional[List[str]] = Field(
|
||||
None,
|
||||
description="Update target list: keep state for existing, capture fresh for new, drop removed",
|
||||
)
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
|
||||
+6
-2
@@ -53,7 +53,9 @@ class EngineInfo(BaseModel):
|
||||
name: str = Field(description="Human-readable engine name")
|
||||
default_config: Dict = Field(description="Default configuration for this engine")
|
||||
available: bool = Field(description="Whether engine is available on this system")
|
||||
has_own_displays: bool = Field(default=False, description="Engine has its own device list (not desktop monitors)")
|
||||
has_own_displays: bool = Field(
|
||||
default=False, description="Engine has its own device list (not desktop monitors)"
|
||||
)
|
||||
|
||||
|
||||
class EngineListResponse(BaseModel):
|
||||
@@ -76,4 +78,6 @@ class TemplateTestRequest(BaseModel):
|
||||
engine_config: Dict = Field(default={}, description="Engine configuration")
|
||||
display_index: int = Field(description="Display index to capture")
|
||||
border_width: int = Field(default=10, ge=1, le=100, description="Border width in pixels")
|
||||
capture_duration: float = Field(default=5.0, ge=1.0, le=30.0, description="Duration to capture in seconds")
|
||||
capture_duration: float = Field(
|
||||
default=5.0, ge=1.0, le=30.0, description="Duration to capture in seconds"
|
||||
)
|
||||
+24
-8
@@ -10,11 +10,19 @@ class WeatherSourceCreate(BaseModel):
|
||||
"""Request to create a weather source."""
|
||||
|
||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||
provider: Literal["open_meteo"] = Field(default="open_meteo", description="Weather data provider")
|
||||
provider: Literal["open_meteo"] = Field(
|
||||
default="open_meteo", description="Weather data provider"
|
||||
)
|
||||
provider_config: Optional[Dict] = Field(None, description="Provider-specific configuration")
|
||||
latitude: float = Field(default=50.0, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0)
|
||||
longitude: float = Field(default=0.0, description="Geographic longitude (-180 to 180)", ge=-180.0, le=180.0)
|
||||
update_interval: int = Field(default=600, description="API poll interval in seconds (60-3600)", ge=60, le=3600)
|
||||
latitude: float = Field(
|
||||
default=50.0, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0
|
||||
)
|
||||
longitude: float = Field(
|
||||
default=0.0, description="Geographic longitude (-180 to 180)", ge=-180.0, le=180.0
|
||||
)
|
||||
update_interval: int = Field(
|
||||
default=600, description="API poll interval in seconds (60-3600)", ge=60, le=3600
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
|
||||
@@ -25,9 +33,15 @@ class WeatherSourceUpdate(BaseModel):
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
provider: Optional[Literal["open_meteo"]] = Field(None, description="Weather data provider")
|
||||
provider_config: Optional[Dict] = Field(None, description="Provider-specific configuration")
|
||||
latitude: Optional[float] = Field(None, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0)
|
||||
longitude: Optional[float] = Field(None, description="Geographic longitude (-180 to 180)", ge=-180.0, le=180.0)
|
||||
update_interval: Optional[int] = Field(None, description="API poll interval in seconds (60-3600)", ge=60, le=3600)
|
||||
latitude: Optional[float] = Field(
|
||||
None, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0
|
||||
)
|
||||
longitude: Optional[float] = Field(
|
||||
None, description="Geographic longitude (-180 to 180)", ge=-180.0, le=180.0
|
||||
)
|
||||
update_interval: Optional[int] = Field(
|
||||
None, description="API poll interval in seconds (60-3600)", ge=60, le=3600
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
@@ -38,7 +52,9 @@ class WeatherSourceResponse(BaseModel):
|
||||
id: str = Field(description="Source ID")
|
||||
name: str = Field(description="Source name")
|
||||
provider: str = Field(description="Weather data provider")
|
||||
provider_config: Dict = Field(default_factory=dict, description="Provider-specific configuration")
|
||||
provider_config: Dict = Field(
|
||||
default_factory=dict, description="Provider-specific configuration"
|
||||
)
|
||||
latitude: float = Field(description="Geographic latitude")
|
||||
longitude: float = Field(description="Geographic longitude")
|
||||
update_interval: int = Field(description="API poll interval in seconds")
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Configuration management for WLED Screen Controller."""
|
||||
"""Configuration management for LedGrab."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Literal
|
||||
|
||||
@@ -8,6 +9,43 @@ import yaml
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
# ── Legacy env var migration ─────────────────────────────────
|
||||
# Warn users who still have WLED_ env vars from pre-rename installs.
|
||||
_OLD_PREFIX = "WLED_"
|
||||
_NEW_PREFIX = "LEDGRAB_"
|
||||
_ENV_MIGRATION_MAP = {
|
||||
"WLED_CONFIG_PATH": "LEDGRAB_CONFIG_PATH",
|
||||
"WLED_DEMO": "LEDGRAB_DEMO",
|
||||
"WLED_RESTART": "LEDGRAB_RESTART",
|
||||
"WLED_TRAY": "LEDGRAB_TRAY",
|
||||
}
|
||||
|
||||
|
||||
def _migrate_legacy_env_vars() -> None:
|
||||
"""Detect old WLED_ env vars and auto-forward them to LEDGRAB_ equivalents."""
|
||||
migrated = []
|
||||
for old_key, new_key in _ENV_MIGRATION_MAP.items():
|
||||
old_val = os.environ.get(old_key)
|
||||
if old_val is not None and os.environ.get(new_key) is None:
|
||||
os.environ[new_key] = old_val
|
||||
migrated.append(f" {old_key} -> {new_key}")
|
||||
# Also forward any WLED_<nested> vars (e.g. WLED_SERVER__PORT)
|
||||
for key in list(os.environ):
|
||||
if key.startswith(_OLD_PREFIX) and key not in _ENV_MIGRATION_MAP:
|
||||
new_key = _NEW_PREFIX + key[len(_OLD_PREFIX) :]
|
||||
if os.environ.get(new_key) is None:
|
||||
os.environ[new_key] = os.environ[key]
|
||||
migrated.append(f" {key} -> {new_key}")
|
||||
if migrated:
|
||||
print(
|
||||
"WARNING: Detected legacy WLED_ environment variables. "
|
||||
"The app was renamed to LedGrab — please update your env vars:\n" + "\n".join(migrated),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
_migrate_legacy_env_vars()
|
||||
|
||||
|
||||
class ServerConfig(BaseSettings):
|
||||
"""Server configuration."""
|
||||
@@ -27,8 +65,8 @@ class AuthConfig(BaseSettings):
|
||||
class AssetsConfig(BaseSettings):
|
||||
"""Assets configuration."""
|
||||
|
||||
max_file_size_mb: int = 50 # Max upload size in MB
|
||||
assets_dir: str = "data/assets" # Directory for uploaded asset files
|
||||
max_file_size_mb: int = 50 # Max upload size in MB
|
||||
assets_dir: str = "data/assets" # Directory for uploaded asset files
|
||||
|
||||
|
||||
class StorageConfig(BaseSettings):
|
||||
@@ -53,7 +91,7 @@ class LoggingConfig(BaseSettings):
|
||||
"""Logging configuration."""
|
||||
|
||||
format: Literal["json", "text"] = "json"
|
||||
file: str = "logs/wled_controller.log"
|
||||
file: str = "logs/ledgrab.log"
|
||||
max_size_mb: int = 100
|
||||
backup_count: int = 5
|
||||
|
||||
@@ -62,7 +100,7 @@ class Config(BaseSettings):
|
||||
"""Main application configuration."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="WLED_",
|
||||
env_prefix="LEDGRAB_",
|
||||
env_nested_delimiter="__",
|
||||
case_sensitive=False,
|
||||
)
|
||||
@@ -112,21 +150,21 @@ class Config(BaseSettings):
|
||||
"""Load configuration from default locations.
|
||||
|
||||
Tries to load from:
|
||||
1. Environment variable WLED_CONFIG_PATH
|
||||
2. WLED_DEMO=true → ./config/demo_config.yaml (if it exists)
|
||||
1. Environment variable LEDGRAB_CONFIG_PATH
|
||||
2. LEDGRAB_DEMO=true → ./config/demo_config.yaml (if it exists)
|
||||
3. ./config/default_config.yaml
|
||||
4. Default values
|
||||
|
||||
Returns:
|
||||
Config instance
|
||||
"""
|
||||
config_path = os.getenv("WLED_CONFIG_PATH")
|
||||
config_path = os.getenv("LEDGRAB_CONFIG_PATH")
|
||||
|
||||
if config_path:
|
||||
return cls.from_yaml(config_path)
|
||||
|
||||
# Demo mode: try dedicated demo config first
|
||||
if os.getenv("WLED_DEMO", "").lower() in ("true", "1", "yes"):
|
||||
if os.getenv("LEDGRAB_DEMO", "").lower() in ("true", "1", "yes"):
|
||||
demo_path = Path("config/demo_config.yaml")
|
||||
if demo_path.exists():
|
||||
return cls.from_yaml(demo_path)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Core functionality for screen capture and WLED control."""
|
||||
|
||||
from wled_controller.core.capture.screen_capture import (
|
||||
from ledgrab.core.capture.screen_capture import (
|
||||
get_available_displays,
|
||||
capture_display,
|
||||
extract_border_pixels,
|
||||
+6
-6
@@ -1,21 +1,21 @@
|
||||
"""Audio capture engine abstraction layer."""
|
||||
|
||||
from wled_controller.core.audio.base import (
|
||||
from ledgrab.core.audio.base import (
|
||||
AudioCaptureEngine,
|
||||
AudioCaptureStreamBase,
|
||||
AudioDeviceInfo,
|
||||
)
|
||||
from wled_controller.core.audio.factory import AudioEngineRegistry
|
||||
from wled_controller.core.audio.analysis import (
|
||||
from ledgrab.core.audio.factory import AudioEngineRegistry
|
||||
from ledgrab.core.audio.analysis import (
|
||||
AudioAnalysis,
|
||||
AudioAnalyzer,
|
||||
NUM_BANDS,
|
||||
DEFAULT_SAMPLE_RATE,
|
||||
DEFAULT_CHUNK_SIZE,
|
||||
)
|
||||
from wled_controller.core.audio.wasapi_engine import WasapiEngine, WasapiCaptureStream
|
||||
from wled_controller.core.audio.sounddevice_engine import SounddeviceEngine, SounddeviceCaptureStream
|
||||
from wled_controller.core.audio.demo_engine import DemoAudioEngine, DemoAudioCaptureStream
|
||||
from ledgrab.core.audio.wasapi_engine import WasapiEngine, WasapiCaptureStream
|
||||
from ledgrab.core.audio.sounddevice_engine import SounddeviceEngine, SounddeviceCaptureStream
|
||||
from ledgrab.core.audio.demo_engine import DemoAudioEngine, DemoAudioCaptureStream
|
||||
|
||||
# Auto-register available engines
|
||||
AudioEngineRegistry.register(WasapiEngine)
|
||||
+42
-23
@@ -40,7 +40,9 @@ class AudioAnalysis:
|
||||
left_rms: float = 0.0
|
||||
left_spectrum: np.ndarray = field(default_factory=lambda: np.zeros(NUM_BANDS, dtype=np.float32))
|
||||
right_rms: float = 0.0
|
||||
right_spectrum: np.ndarray = field(default_factory=lambda: np.zeros(NUM_BANDS, dtype=np.float32))
|
||||
right_spectrum: np.ndarray = field(
|
||||
default_factory=lambda: np.zeros(NUM_BANDS, dtype=np.float32)
|
||||
)
|
||||
|
||||
|
||||
def _build_log_bands(num_bands: int, fft_size: int, sample_rate: int) -> List[Tuple[int, int]]:
|
||||
@@ -74,7 +76,9 @@ class AudioAnalyzer:
|
||||
thread calls analyze() (the capture thread).
|
||||
"""
|
||||
|
||||
def __init__(self, sample_rate: int = DEFAULT_SAMPLE_RATE, chunk_size: int = DEFAULT_CHUNK_SIZE):
|
||||
def __init__(
|
||||
self, sample_rate: int = DEFAULT_SAMPLE_RATE, chunk_size: int = DEFAULT_CHUNK_SIZE
|
||||
):
|
||||
self._sample_rate = sample_rate
|
||||
self._chunk_size = chunk_size
|
||||
|
||||
@@ -102,12 +106,18 @@ class AudioAnalyzer:
|
||||
# Double-buffered output spectra — avoids allocating new arrays each
|
||||
# analyze() call. Consumers hold a reference to the "old" buffer while
|
||||
# the analyzer writes into the alternate one.
|
||||
self._out_spectrum = [np.zeros(NUM_BANDS, dtype=np.float32),
|
||||
np.zeros(NUM_BANDS, dtype=np.float32)]
|
||||
self._out_spectrum_left = [np.zeros(NUM_BANDS, dtype=np.float32),
|
||||
np.zeros(NUM_BANDS, dtype=np.float32)]
|
||||
self._out_spectrum_right = [np.zeros(NUM_BANDS, dtype=np.float32),
|
||||
np.zeros(NUM_BANDS, dtype=np.float32)]
|
||||
self._out_spectrum = [
|
||||
np.zeros(NUM_BANDS, dtype=np.float32),
|
||||
np.zeros(NUM_BANDS, dtype=np.float32),
|
||||
]
|
||||
self._out_spectrum_left = [
|
||||
np.zeros(NUM_BANDS, dtype=np.float32),
|
||||
np.zeros(NUM_BANDS, dtype=np.float32),
|
||||
]
|
||||
self._out_spectrum_right = [
|
||||
np.zeros(NUM_BANDS, dtype=np.float32),
|
||||
np.zeros(NUM_BANDS, dtype=np.float32),
|
||||
]
|
||||
self._out_idx = 0 # toggles 0/1 each analyze() call
|
||||
|
||||
# Pre-compute band start/end arrays and widths for vectorized binning
|
||||
@@ -147,14 +157,14 @@ class AudioAnalyzer:
|
||||
# Split channels and mix to mono
|
||||
if channels > 1:
|
||||
data = raw_data.reshape(-1, channels)
|
||||
np.copyto(self._left_buf[:len(data)], data[:, 0])
|
||||
np.copyto(self._left_buf[: len(data)], data[:, 0])
|
||||
right_col = data[:, 1] if channels >= 2 else data[:, 0]
|
||||
np.copyto(self._right_buf[:len(data)], right_col)
|
||||
np.add(data[:, 0], right_col, out=self._mono_buf[:len(data)])
|
||||
self._mono_buf[:len(data)] *= 0.5
|
||||
samples = self._mono_buf[:len(data)]
|
||||
left_samples = self._left_buf[:len(data)]
|
||||
right_samples = self._right_buf[:len(data)]
|
||||
np.copyto(self._right_buf[: len(data)], right_col)
|
||||
np.add(data[:, 0], right_col, out=self._mono_buf[: len(data)])
|
||||
self._mono_buf[: len(data)] *= 0.5
|
||||
samples = self._mono_buf[: len(data)]
|
||||
left_samples = self._left_buf[: len(data)]
|
||||
right_samples = self._right_buf[: len(data)]
|
||||
else:
|
||||
samples = raw_data
|
||||
left_samples = samples
|
||||
@@ -176,13 +186,22 @@ class AudioAnalyzer:
|
||||
right_rms = rms
|
||||
|
||||
# FFT for mono, left, right
|
||||
self._fft_bands(samples, self._spectrum_buf, self._smooth_spectrum,
|
||||
alpha, one_minus_alpha)
|
||||
self._fft_bands(samples, self._spectrum_buf, self._smooth_spectrum, alpha, one_minus_alpha)
|
||||
if channels > 1:
|
||||
self._fft_bands(left_samples, self._spectrum_buf_left, self._smooth_spectrum_left,
|
||||
alpha, one_minus_alpha)
|
||||
self._fft_bands(right_samples, self._spectrum_buf_right, self._smooth_spectrum_right,
|
||||
alpha, one_minus_alpha)
|
||||
self._fft_bands(
|
||||
left_samples,
|
||||
self._spectrum_buf_left,
|
||||
self._smooth_spectrum_left,
|
||||
alpha,
|
||||
one_minus_alpha,
|
||||
)
|
||||
self._fft_bands(
|
||||
right_samples,
|
||||
self._spectrum_buf_right,
|
||||
self._smooth_spectrum_right,
|
||||
alpha,
|
||||
one_minus_alpha,
|
||||
)
|
||||
else:
|
||||
np.copyto(self._smooth_spectrum_left, self._smooth_spectrum)
|
||||
np.copyto(self._smooth_spectrum_right, self._smooth_spectrum)
|
||||
@@ -233,7 +252,7 @@ class AudioAnalyzer:
|
||||
chunk = np.pad(chunk, (0, chunk_size - len(chunk)))
|
||||
np.multiply(chunk, self._window, out=self._fft_windowed)
|
||||
fft_mag = np.abs(np.fft.rfft(self._fft_windowed))
|
||||
fft_mag *= (1.0 / chunk_size)
|
||||
fft_mag *= 1.0 / chunk_size
|
||||
fft_len = len(fft_mag)
|
||||
# Vectorized band binning using cumulative sum
|
||||
valid = (self._band_starts < fft_len) & (self._band_ends <= fft_len) & (self._band_ends > 0)
|
||||
@@ -246,6 +265,6 @@ class AudioAnalyzer:
|
||||
buf[valid] = band_sums / self._band_widths[valid]
|
||||
spec_max = float(np.max(buf))
|
||||
if spec_max > 1e-6:
|
||||
buf *= (1.0 / spec_max)
|
||||
buf *= 1.0 / spec_max
|
||||
smooth_buf *= one_minus_alpha
|
||||
smooth_buf += alpha * buf
|
||||
+39
-27
@@ -13,13 +13,13 @@ import threading
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from wled_controller.core.audio.analysis import (
|
||||
from ledgrab.core.audio.analysis import (
|
||||
AudioAnalysis,
|
||||
AudioAnalyzer,
|
||||
)
|
||||
from wled_controller.core.audio.base import AudioCaptureStreamBase
|
||||
from wled_controller.core.audio.factory import AudioEngineRegistry
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.audio.base import AudioCaptureStreamBase
|
||||
from ledgrab.core.audio.factory import AudioEngineRegistry
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -36,6 +36,7 @@ __all__ = [
|
||||
# ManagedAudioStream — wraps engine stream + analyzer in background thread
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ManagedAudioStream:
|
||||
"""Wraps an AudioCaptureStreamBase + AudioAnalyzer in a background thread.
|
||||
|
||||
@@ -66,9 +67,10 @@ class ManagedAudioStream:
|
||||
return
|
||||
self._running = True
|
||||
self._thread = threading.Thread(
|
||||
target=self._capture_loop, daemon=True,
|
||||
target=self._capture_loop,
|
||||
daemon=True,
|
||||
name=f"AudioCapture-{self._engine_type}-{self._device_index}-"
|
||||
f"{'lb' if self._is_loopback else 'in'}",
|
||||
f"{'lb' if self._is_loopback else 'in'}",
|
||||
)
|
||||
self._thread.start()
|
||||
logger.info(
|
||||
@@ -99,8 +101,10 @@ class ManagedAudioStream:
|
||||
stream: Optional[AudioCaptureStreamBase] = None
|
||||
try:
|
||||
stream = AudioEngineRegistry.create_stream(
|
||||
self._engine_type, self._device_index,
|
||||
self._is_loopback, self._engine_config,
|
||||
self._engine_type,
|
||||
self._device_index,
|
||||
self._is_loopback,
|
||||
self._engine_config,
|
||||
)
|
||||
stream.initialize()
|
||||
|
||||
@@ -155,6 +159,7 @@ class ManagedAudioStream:
|
||||
# AudioCaptureManager — ref-counted shared capture streams
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AudioCaptureManager:
|
||||
"""Manages shared ManagedAudioStream instances with reference counting.
|
||||
|
||||
@@ -201,7 +206,10 @@ class AudioCaptureManager:
|
||||
return stream
|
||||
|
||||
stream = ManagedAudioStream(
|
||||
engine_type, device_index, is_loopback, engine_config,
|
||||
engine_type,
|
||||
device_index,
|
||||
is_loopback,
|
||||
engine_config,
|
||||
)
|
||||
stream.start()
|
||||
self._streams[key] = (stream, 1)
|
||||
@@ -279,15 +287,17 @@ class AudioCaptureManager:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
result.append({
|
||||
"index": dev.index,
|
||||
"name": dev.name,
|
||||
"is_input": dev.is_input,
|
||||
"is_loopback": dev.is_loopback,
|
||||
"channels": dev.channels,
|
||||
"default_samplerate": dev.default_samplerate,
|
||||
"engine_type": engine_type,
|
||||
})
|
||||
result.append(
|
||||
{
|
||||
"index": dev.index,
|
||||
"name": dev.name,
|
||||
"is_input": dev.is_input,
|
||||
"is_loopback": dev.is_loopback,
|
||||
"channels": dev.channels,
|
||||
"default_samplerate": dev.default_samplerate,
|
||||
"engine_type": engine_type,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error enumerating devices for engine '{engine_type}': {e}")
|
||||
return result
|
||||
@@ -306,15 +316,17 @@ class AudioCaptureManager:
|
||||
continue
|
||||
devices = []
|
||||
for dev in engine_class.enumerate_devices():
|
||||
devices.append({
|
||||
"index": dev.index,
|
||||
"name": dev.name,
|
||||
"is_input": dev.is_input,
|
||||
"is_loopback": dev.is_loopback,
|
||||
"channels": dev.channels,
|
||||
"default_samplerate": dev.default_samplerate,
|
||||
"engine_type": engine_type,
|
||||
})
|
||||
devices.append(
|
||||
{
|
||||
"index": dev.index,
|
||||
"name": dev.name,
|
||||
"is_input": dev.is_input,
|
||||
"is_loopback": dev.is_loopback,
|
||||
"channels": dev.channels,
|
||||
"default_samplerate": dev.default_samplerate,
|
||||
"engine_type": engine_type,
|
||||
}
|
||||
)
|
||||
result[engine_type] = devices
|
||||
except Exception as e:
|
||||
logger.error(f"Error enumerating devices for engine '{engine_type}': {e}")
|
||||
+1
-1
@@ -9,7 +9,7 @@ from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import NUM_BANDS
|
||||
from ledgrab.core.audio.analysis import NUM_BANDS
|
||||
|
||||
|
||||
def compute_band_mask(freq_low: float, freq_high: float) -> np.ndarray:
|
||||
+13
-11
@@ -5,13 +5,13 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.config import is_demo_mode
|
||||
from wled_controller.core.audio.base import (
|
||||
from ledgrab.config import is_demo_mode
|
||||
from ledgrab.core.audio.base import (
|
||||
AudioCaptureEngine,
|
||||
AudioCaptureStreamBase,
|
||||
AudioDeviceInfo,
|
||||
)
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -126,14 +126,16 @@ class DemoAudioEngine(AudioCaptureEngine):
|
||||
def enumerate_devices(cls) -> List[AudioDeviceInfo]:
|
||||
devices = []
|
||||
for idx, (name, is_loopback, channels, samplerate) in enumerate(_VIRTUAL_DEVICES):
|
||||
devices.append(AudioDeviceInfo(
|
||||
index=idx,
|
||||
name=name,
|
||||
is_input=True,
|
||||
is_loopback=is_loopback,
|
||||
channels=channels,
|
||||
default_samplerate=samplerate,
|
||||
))
|
||||
devices.append(
|
||||
AudioDeviceInfo(
|
||||
index=idx,
|
||||
name=name,
|
||||
is_input=True,
|
||||
is_loopback=is_loopback,
|
||||
channels=channels,
|
||||
default_samplerate=samplerate,
|
||||
)
|
||||
)
|
||||
logger.debug(f"Demo audio engine: {len(devices)} virtual device(s)")
|
||||
return devices
|
||||
|
||||
+7
-15
@@ -2,9 +2,9 @@
|
||||
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from wled_controller.core.audio.base import AudioCaptureEngine, AudioCaptureStreamBase
|
||||
from wled_controller.config import is_demo_mode
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.audio.base import AudioCaptureEngine, AudioCaptureStreamBase
|
||||
from ledgrab.config import is_demo_mode
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -78,9 +78,7 @@ class AudioEngineRegistry:
|
||||
if engine_class.is_available():
|
||||
available.append(engine_type)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error checking availability for audio engine '{engine_type}': {e}"
|
||||
)
|
||||
logger.error(f"Error checking availability for audio engine '{engine_type}': {e}")
|
||||
return available
|
||||
|
||||
@classmethod
|
||||
@@ -101,9 +99,7 @@ class AudioEngineRegistry:
|
||||
best_priority = engine_class.ENGINE_PRIORITY
|
||||
best_type = engine_type
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error checking availability for audio engine '{engine_type}': {e}"
|
||||
)
|
||||
logger.error(f"Error checking availability for audio engine '{engine_type}': {e}")
|
||||
return best_type
|
||||
|
||||
@classmethod
|
||||
@@ -144,9 +140,7 @@ class AudioEngineRegistry:
|
||||
engine_class = cls.get_engine(engine_type)
|
||||
|
||||
if not engine_class.is_available():
|
||||
raise ValueError(
|
||||
f"Audio engine '{engine_type}' is not available on this system"
|
||||
)
|
||||
raise ValueError(f"Audio engine '{engine_type}' is not available on this system")
|
||||
|
||||
try:
|
||||
stream = engine_class.create_stream(device_index, is_loopback, config)
|
||||
@@ -157,9 +151,7 @@ class AudioEngineRegistry:
|
||||
return stream
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create stream for audio engine '{engine_type}': {e}")
|
||||
raise RuntimeError(
|
||||
f"Failed to create stream for audio engine '{engine_type}': {e}"
|
||||
)
|
||||
raise RuntimeError(f"Failed to create stream for audio engine '{engine_type}': {e}")
|
||||
|
||||
@classmethod
|
||||
def clear_registry(cls):
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Audio filter system.
|
||||
|
||||
Provides a pluggable filter architecture for audio analysis postprocessing.
|
||||
Import this package to ensure all built-in filters are registered.
|
||||
"""
|
||||
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.pipeline import AudioFilterPipeline
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
# Import individual filters to trigger auto-registration
|
||||
import ledgrab.core.audio.filters.audio_filter_template # noqa: F401
|
||||
import ledgrab.core.audio.filters.channel_extract # noqa: F401
|
||||
import ledgrab.core.audio.filters.band_extract # noqa: F401
|
||||
import ledgrab.core.audio.filters.peak_hold # noqa: F401
|
||||
import ledgrab.core.audio.filters.gain # noqa: F401
|
||||
import ledgrab.core.audio.filters.noise_gate # noqa: F401
|
||||
import ledgrab.core.audio.filters.envelope_follower # noqa: F401
|
||||
import ledgrab.core.audio.filters.spectral_smoothing # noqa: F401
|
||||
import ledgrab.core.audio.filters.compressor # noqa: F401
|
||||
import ledgrab.core.audio.filters.inverter # noqa: F401
|
||||
import ledgrab.core.audio.filters.beat_gate # noqa: F401
|
||||
import ledgrab.core.audio.filters.delay # noqa: F401
|
||||
import ledgrab.core.audio.filters.auto_gain # noqa: F401
|
||||
|
||||
__all__ = [
|
||||
"AudioFilter",
|
||||
"AudioFilterOptionDef",
|
||||
"AudioFilterPipeline",
|
||||
"AudioFilterRegistry",
|
||||
]
|
||||
+3
-3
@@ -7,9 +7,9 @@ referenced template's filters when building the filter chain.
|
||||
|
||||
from typing import List
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
@AudioFilterRegistry.register
|
||||
+3
-3
@@ -10,9 +10,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
@AudioFilterRegistry.register
|
||||
+4
-4
@@ -3,10 +3,10 @@
|
||||
from dataclasses import replace
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from wled_controller.core.audio.band_filter import apply_band_filter, compute_band_mask
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.band_filter import apply_band_filter, compute_band_mask
|
||||
|
||||
|
||||
# Preset frequency ranges
|
||||
+1
-1
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
|
||||
|
||||
@dataclass
|
||||
+3
-3
@@ -6,9 +6,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
_ZERO_SPECTRUM = np.zeros(NUM_BANDS, dtype=np.float32)
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
@AudioFilterRegistry.register
|
||||
+3
-3
@@ -5,9 +5,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
@AudioFilterRegistry.register
|
||||
+3
-3
@@ -6,9 +6,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
# Assumed update rate for sizing the ring buffer
|
||||
_UPDATE_RATE_HZ = 30
|
||||
+3
-3
@@ -6,9 +6,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
def _time_constant_coeff(time_ms: float, dt: float) -> float:
|
||||
+3
-3
@@ -5,9 +5,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
@AudioFilterRegistry.register
|
||||
+3
-3
@@ -5,9 +5,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
@AudioFilterRegistry.register
|
||||
+3
-3
@@ -5,9 +5,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
_ZERO_SPECTRUM = np.zeros(NUM_BANDS, dtype=np.float32)
|
||||
|
||||
+3
-3
@@ -6,9 +6,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
@AudioFilterRegistry.register
|
||||
+6
-6
@@ -13,14 +13,14 @@ from __future__ import annotations
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from wled_controller.core.filters.filter_instance import FilterInstance
|
||||
from wled_controller.storage.audio_processing_template_store import (
|
||||
from ledgrab.core.filters.filter_instance import FilterInstance
|
||||
from ledgrab.storage.audio_processing_template_store import (
|
||||
AudioProcessingTemplateStore,
|
||||
)
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
|
||||
from typing import Dict, Type
|
||||
|
||||
from wled_controller.core.audio.filters.base import AudioFilter
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.core.audio.filters.base import AudioFilter
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@ from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.analysis import NUM_BANDS, AudioAnalysis
|
||||
from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
@AudioFilterRegistry.register
|
||||
+14
-13
@@ -4,12 +4,12 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.base import (
|
||||
from ledgrab.core.audio.base import (
|
||||
AudioCaptureEngine,
|
||||
AudioCaptureStreamBase,
|
||||
AudioDeviceInfo,
|
||||
)
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -51,9 +51,7 @@ class SounddeviceCaptureStream(AudioCaptureStreamBase):
|
||||
dev_info = sd.query_devices(device_id)
|
||||
self._channels = min(self._channels, int(dev_info["max_input_channels"]))
|
||||
if self._channels < 1:
|
||||
raise RuntimeError(
|
||||
f"Device {device_id} ({dev_info['name']}) has no input channels"
|
||||
)
|
||||
raise RuntimeError(f"Device {device_id} ({dev_info['name']}) has no input channels")
|
||||
self._sample_rate = int(dev_info["default_samplerate"])
|
||||
|
||||
self._sd_stream = sd.InputStream(
|
||||
@@ -104,6 +102,7 @@ class SounddeviceEngine(AudioCaptureEngine):
|
||||
def is_available(cls) -> bool:
|
||||
try:
|
||||
import sounddevice # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError as e:
|
||||
logger.debug("Sounddevice engine unavailable: %s", e)
|
||||
@@ -136,14 +135,16 @@ class SounddeviceEngine(AudioCaptureEngine):
|
||||
# On PulseAudio/PipeWire, monitor sources are loopback-capable
|
||||
is_loopback = "monitor" in name.lower()
|
||||
|
||||
result.append(AudioDeviceInfo(
|
||||
index=i,
|
||||
name=name,
|
||||
is_input=True,
|
||||
is_loopback=is_loopback,
|
||||
channels=max_in,
|
||||
default_samplerate=dev["default_samplerate"],
|
||||
))
|
||||
result.append(
|
||||
AudioDeviceInfo(
|
||||
index=i,
|
||||
name=name,
|
||||
is_input=True,
|
||||
is_loopback=is_loopback,
|
||||
channels=max_in,
|
||||
default_samplerate=dev["default_samplerate"],
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
+23
-18
@@ -4,12 +4,12 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.audio.base import (
|
||||
from ledgrab.core.audio.base import (
|
||||
AudioCaptureEngine,
|
||||
AudioCaptureStreamBase,
|
||||
AudioDeviceInfo,
|
||||
)
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -140,6 +140,7 @@ class WasapiEngine(AudioCaptureEngine):
|
||||
def is_available(cls) -> bool:
|
||||
try:
|
||||
import pyaudiowpatch # noqa: F401
|
||||
|
||||
return True
|
||||
except ImportError as e:
|
||||
logger.debug("WASAPI engine unavailable (pyaudiowpatch not installed): %s", e)
|
||||
@@ -187,14 +188,16 @@ class WasapiEngine(AudioCaptureEngine):
|
||||
if is_loopback:
|
||||
loopback_names.add(name)
|
||||
|
||||
result.append(AudioDeviceInfo(
|
||||
index=i,
|
||||
name=name,
|
||||
is_input=True,
|
||||
is_loopback=is_loopback,
|
||||
channels=dev["maxInputChannels"],
|
||||
default_samplerate=dev["defaultSampleRate"],
|
||||
))
|
||||
result.append(
|
||||
AudioDeviceInfo(
|
||||
index=i,
|
||||
name=name,
|
||||
is_input=True,
|
||||
is_loopback=is_loopback,
|
||||
channels=dev["maxInputChannels"],
|
||||
default_samplerate=dev["defaultSampleRate"],
|
||||
)
|
||||
)
|
||||
|
||||
# Second pass: add loopback entries for output devices that
|
||||
# don't already have a dedicated loopback input endpoint.
|
||||
@@ -209,14 +212,16 @@ class WasapiEngine(AudioCaptureEngine):
|
||||
if loopback_name in loopback_names:
|
||||
continue # already covered by a dedicated loopback endpoint
|
||||
|
||||
result.append(AudioDeviceInfo(
|
||||
index=i,
|
||||
name=loopback_name,
|
||||
is_input=False,
|
||||
is_loopback=True,
|
||||
channels=dev["maxOutputChannels"],
|
||||
default_samplerate=dev["defaultSampleRate"],
|
||||
))
|
||||
result.append(
|
||||
AudioDeviceInfo(
|
||||
index=i,
|
||||
name=loopback_name,
|
||||
is_input=False,
|
||||
is_loopback=True,
|
||||
channels=dev["maxOutputChannels"],
|
||||
default_samplerate=dev["defaultSampleRate"],
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
+9
-9
@@ -5,8 +5,8 @@ import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Optional, Set
|
||||
|
||||
from wled_controller.core.automations.platform_detector import PlatformDetector
|
||||
from wled_controller.storage.automation import (
|
||||
from ledgrab.core.automations.platform_detector import PlatformDetector
|
||||
from ledgrab.storage.automation import (
|
||||
ApplicationRule,
|
||||
Automation,
|
||||
DisplayStateRule,
|
||||
@@ -18,9 +18,9 @@ from wled_controller.storage.automation import (
|
||||
TimeOfDayRule,
|
||||
WebhookRule,
|
||||
)
|
||||
from wled_controller.storage.automation_store import AutomationStore
|
||||
from wled_controller.storage.scene_preset import ScenePreset
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.storage.automation_store import AutomationStore
|
||||
from ledgrab.storage.scene_preset import ScenePreset
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -500,7 +500,7 @@ class AutomationEngine:
|
||||
|
||||
# For "revert" mode, capture current state before activating
|
||||
if automation.deactivation_mode == "revert":
|
||||
from wled_controller.core.scenes.scene_activator import capture_current_snapshot
|
||||
from ledgrab.core.scenes.scene_activator import capture_current_snapshot
|
||||
|
||||
targets = capture_current_snapshot(self._target_store, self._manager)
|
||||
self._pre_activation_snapshots[automation.id] = ScenePreset(
|
||||
@@ -510,7 +510,7 @@ class AutomationEngine:
|
||||
)
|
||||
|
||||
# Apply the scene
|
||||
from wled_controller.core.scenes.scene_activator import apply_scene_state
|
||||
from ledgrab.core.scenes.scene_activator import apply_scene_state
|
||||
|
||||
status, errors = await apply_scene_state(
|
||||
preset,
|
||||
@@ -556,7 +556,7 @@ class AutomationEngine:
|
||||
"""Revert to pre-activation snapshot."""
|
||||
snapshot = self._pre_activation_snapshots.pop(automation_id, None)
|
||||
if snapshot and self._target_store:
|
||||
from wled_controller.core.scenes.scene_activator import apply_scene_state
|
||||
from ledgrab.core.scenes.scene_activator import apply_scene_state
|
||||
|
||||
status, errors = await apply_scene_state(
|
||||
snapshot,
|
||||
@@ -576,7 +576,7 @@ class AutomationEngine:
|
||||
if fallback_id and self._scene_preset_store and self._target_store:
|
||||
try:
|
||||
fallback = self._scene_preset_store.get_preset(fallback_id)
|
||||
from wled_controller.core.scenes.scene_activator import apply_scene_state
|
||||
from ledgrab.core.scenes.scene_activator import apply_scene_state
|
||||
|
||||
status, errors = await apply_scene_state(
|
||||
fallback,
|
||||
+43
-10
@@ -12,7 +12,7 @@ import sys
|
||||
import threading
|
||||
from typing import Optional, Set
|
||||
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -56,13 +56,29 @@ class PlatformDetector:
|
||||
|
||||
# GUID_CONSOLE_DISPLAY_STATE = {6FE69556-704A-47A0-8F24-C28D936FDA47}
|
||||
GUID_CONSOLE_DISPLAY_STATE = (ctypes.c_ubyte * 16)(
|
||||
0x56, 0x95, 0xE6, 0x6F, 0x4A, 0x70, 0xA0, 0x47,
|
||||
0x8F, 0x24, 0xC2, 0x8D, 0x93, 0x6F, 0xDA, 0x47,
|
||||
0x56,
|
||||
0x95,
|
||||
0xE6,
|
||||
0x6F,
|
||||
0x4A,
|
||||
0x70,
|
||||
0xA0,
|
||||
0x47,
|
||||
0x8F,
|
||||
0x24,
|
||||
0xC2,
|
||||
0x8D,
|
||||
0x93,
|
||||
0x6F,
|
||||
0xDA,
|
||||
0x47,
|
||||
)
|
||||
|
||||
user32.DefWindowProcW.argtypes = [
|
||||
ctypes.wintypes.HWND, ctypes.c_uint,
|
||||
ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM,
|
||||
ctypes.wintypes.HWND,
|
||||
ctypes.c_uint,
|
||||
ctypes.wintypes.WPARAM,
|
||||
ctypes.wintypes.LPARAM,
|
||||
]
|
||||
user32.DefWindowProcW.restype = ctypes.c_ssize_t
|
||||
|
||||
@@ -113,8 +129,18 @@ class PlatformDetector:
|
||||
|
||||
HWND_MESSAGE = ctypes.wintypes.HWND(-3)
|
||||
hwnd = user32.CreateWindowExW(
|
||||
0, wc.lpszClassName, "LedGrab Display Monitor",
|
||||
0, 0, 0, 0, 0, HWND_MESSAGE, None, wc.hInstance, None,
|
||||
0,
|
||||
wc.lpszClassName,
|
||||
"LedGrab Display Monitor",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
HWND_MESSAGE,
|
||||
None,
|
||||
wc.hInstance,
|
||||
None,
|
||||
)
|
||||
if not hwnd:
|
||||
logger.warning("Failed to create display monitor hidden window")
|
||||
@@ -153,6 +179,7 @@ class PlatformDetector:
|
||||
return None
|
||||
|
||||
try:
|
||||
|
||||
class LASTINPUTINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", ctypes.c_uint),
|
||||
@@ -192,7 +219,8 @@ class PlatformDetector:
|
||||
pid_array = (ctypes.wintypes.DWORD * 2048)()
|
||||
cb_needed = ctypes.wintypes.DWORD()
|
||||
psapi.EnumProcesses(
|
||||
ctypes.byref(pid_array), ctypes.sizeof(pid_array),
|
||||
ctypes.byref(pid_array),
|
||||
ctypes.sizeof(pid_array),
|
||||
ctypes.byref(cb_needed),
|
||||
)
|
||||
n_pids = cb_needed.value // ctypes.sizeof(ctypes.wintypes.DWORD)
|
||||
@@ -205,14 +233,19 @@ class PlatformDetector:
|
||||
if pid == 0:
|
||||
continue
|
||||
handle = kernel32.OpenProcess(
|
||||
PROCESS_QUERY_LIMITED_INFORMATION, False, pid,
|
||||
PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
False,
|
||||
pid,
|
||||
)
|
||||
if not handle:
|
||||
continue
|
||||
try:
|
||||
buf_size = ctypes.wintypes.DWORD(512)
|
||||
if kernel32.QueryFullProcessImageNameW(
|
||||
handle, 0, name_buf, ctypes.byref(buf_size),
|
||||
handle,
|
||||
0,
|
||||
name_buf,
|
||||
ctypes.byref(buf_size),
|
||||
):
|
||||
procs.add(os.path.basename(name_buf.value).lower())
|
||||
finally:
|
||||
+26
-17
@@ -6,8 +6,8 @@ from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from wled_controller.storage.database import Database
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.storage.database import Database
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -48,11 +48,14 @@ class AutoBackupEngine:
|
||||
return dict(DEFAULT_SETTINGS)
|
||||
|
||||
def _save_settings(self) -> None:
|
||||
self._db.set_setting("auto_backup", {
|
||||
"enabled": self._settings["enabled"],
|
||||
"interval_hours": self._settings["interval_hours"],
|
||||
"max_backups": self._settings["max_backups"],
|
||||
})
|
||||
self._db.set_setting(
|
||||
"auto_backup",
|
||||
{
|
||||
"enabled": self._settings["enabled"],
|
||||
"interval_hours": self._settings["interval_hours"],
|
||||
"max_backups": self._settings["max_backups"],
|
||||
},
|
||||
)
|
||||
|
||||
# ─── Lifecycle ─────────────────────────────────────────────
|
||||
|
||||
@@ -152,7 +155,9 @@ class AutoBackupEngine:
|
||||
"enabled": self._settings["enabled"],
|
||||
"interval_hours": self._settings["interval_hours"],
|
||||
"max_backups": self._settings["max_backups"],
|
||||
"last_backup_time": self._last_backup_time.isoformat() if self._last_backup_time else None,
|
||||
"last_backup_time": (
|
||||
self._last_backup_time.isoformat() if self._last_backup_time else None
|
||||
),
|
||||
"next_backup_time": next_backup,
|
||||
}
|
||||
|
||||
@@ -164,9 +169,7 @@ class AutoBackupEngine:
|
||||
|
||||
if enabled:
|
||||
self._start_loop()
|
||||
logger.info(
|
||||
f"Auto-backup enabled (every {interval_hours}h, max {max_backups})"
|
||||
)
|
||||
logger.info(f"Auto-backup enabled (every {interval_hours}h, max {max_backups})")
|
||||
else:
|
||||
self._cancel_loop()
|
||||
logger.info("Auto-backup disabled")
|
||||
@@ -176,13 +179,19 @@ class AutoBackupEngine:
|
||||
|
||||
def list_backups(self) -> List[dict]:
|
||||
backups = []
|
||||
for f in sorted(self._backup_dir.glob(f"*{_BACKUP_EXT}"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
for f in sorted(
|
||||
self._backup_dir.glob(f"*{_BACKUP_EXT}"), key=lambda p: p.stat().st_mtime, reverse=True
|
||||
):
|
||||
stat = f.stat()
|
||||
backups.append({
|
||||
"filename": f.name,
|
||||
"size_bytes": stat.st_size,
|
||||
"created_at": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
||||
})
|
||||
backups.append(
|
||||
{
|
||||
"filename": f.name,
|
||||
"size_bytes": stat.st_size,
|
||||
"created_at": datetime.fromtimestamp(
|
||||
stat.st_mtime, tz=timezone.utc
|
||||
).isoformat(),
|
||||
}
|
||||
)
|
||||
return backups
|
||||
|
||||
def _safe_backup_path(self, filename: str) -> Path:
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
"""Screen capture and calibration."""
|
||||
|
||||
from wled_controller.core.capture.screen_capture import (
|
||||
from ledgrab.core.capture.screen_capture import (
|
||||
BorderPixels,
|
||||
ScreenCapture,
|
||||
capture_display,
|
||||
extract_border_pixels,
|
||||
get_available_displays,
|
||||
)
|
||||
from wled_controller.core.capture.calibration import (
|
||||
from ledgrab.core.capture.calibration import (
|
||||
CalibrationConfig,
|
||||
PixelMapper,
|
||||
create_default_calibration,
|
||||
+67
-37
@@ -5,13 +5,13 @@ from typing import Dict, List, Literal, Set, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.core.capture.screen_capture import (
|
||||
from ledgrab.core.capture.screen_capture import (
|
||||
BorderPixels,
|
||||
calculate_average_color,
|
||||
calculate_median_color,
|
||||
calculate_dominant_color,
|
||||
)
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -31,9 +31,19 @@ EDGE_ORDER: Dict[Tuple[str, str], List[str]] = {
|
||||
# Whether LEDs are reversed on each edge for each (start_position, layout) combination.
|
||||
EDGE_REVERSE: Dict[Tuple[str, str], Dict[str, bool]] = {
|
||||
("bottom_left", "clockwise"): {"left": True, "top": False, "right": False, "bottom": True},
|
||||
("bottom_left", "counterclockwise"): {"bottom": False, "right": True, "top": True, "left": False},
|
||||
("bottom_left", "counterclockwise"): {
|
||||
"bottom": False,
|
||||
"right": True,
|
||||
"top": True,
|
||||
"left": False,
|
||||
},
|
||||
("bottom_right", "clockwise"): {"bottom": True, "left": True, "top": False, "right": False},
|
||||
("bottom_right", "counterclockwise"): {"right": True, "top": True, "left": False, "bottom": False},
|
||||
("bottom_right", "counterclockwise"): {
|
||||
"right": True,
|
||||
"top": True,
|
||||
"left": False,
|
||||
"bottom": False,
|
||||
},
|
||||
("top_left", "clockwise"): {"top": False, "right": False, "bottom": True, "left": True},
|
||||
("top_left", "counterclockwise"): {"left": False, "bottom": False, "right": True, "top": True},
|
||||
("top_right", "clockwise"): {"right": False, "bottom": True, "left": True, "top": False},
|
||||
@@ -118,12 +128,14 @@ class CalibrationConfig:
|
||||
for edge in edge_order:
|
||||
count = led_counts[edge]
|
||||
if count > 0:
|
||||
segments.append(CalibrationSegment(
|
||||
edge=edge,
|
||||
led_start=led_start,
|
||||
led_count=count,
|
||||
reverse=reverse_map.get(edge, False),
|
||||
))
|
||||
segments.append(
|
||||
CalibrationSegment(
|
||||
edge=edge,
|
||||
led_start=led_start,
|
||||
led_count=count,
|
||||
reverse=reverse_map.get(edge, False),
|
||||
)
|
||||
)
|
||||
led_start += count
|
||||
|
||||
return segments
|
||||
@@ -172,8 +184,12 @@ class CalibrationConfig:
|
||||
if total <= 0:
|
||||
raise ValueError("Calibration must have at least one LED")
|
||||
|
||||
for edge, count in [("top", self.leds_top), ("right", self.leds_right),
|
||||
("bottom", self.leds_bottom), ("left", self.leds_left)]:
|
||||
for edge, count in [
|
||||
("top", self.leds_top),
|
||||
("right", self.leds_right),
|
||||
("bottom", self.leds_bottom),
|
||||
("left", self.leds_left),
|
||||
]:
|
||||
if count < 0:
|
||||
raise ValueError(f"LED count for {edge} must be non-negative, got {count}")
|
||||
|
||||
@@ -182,7 +198,9 @@ class CalibrationConfig:
|
||||
if not (0.0 <= start <= 1.0) or not (0.0 <= end <= 1.0):
|
||||
raise ValueError(f"Span for {edge} must be in [0.0, 1.0], got ({start}, {end})")
|
||||
if end <= start:
|
||||
raise ValueError(f"Span end must be greater than start for {edge}, got ({start}, {end})")
|
||||
raise ValueError(
|
||||
f"Span end must be greater than start for {edge}, got ({start}, {end})"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -376,10 +394,7 @@ class PixelMapper:
|
||||
segment_sums = cumsum_buf[ends] - cumsum_buf[starts]
|
||||
return np.clip(segment_sums / lengths, 0, 255).astype(np.uint8)
|
||||
|
||||
def map_border_to_leds(
|
||||
self,
|
||||
border_pixels: BorderPixels
|
||||
) -> np.ndarray:
|
||||
def map_border_to_leds(self, border_pixels: BorderPixels) -> np.ndarray:
|
||||
"""Map screen border pixels to LED colors.
|
||||
|
||||
Uses pre-allocated buffers and pre-computed index arrays to avoid
|
||||
@@ -402,32 +417,30 @@ class PixelMapper:
|
||||
edge_pixels = self._get_edge_pixels(border_pixels, segment.edge)
|
||||
|
||||
if self._use_fast_avg:
|
||||
colors = self._map_edge_average(
|
||||
edge_pixels, segment.edge, segment.led_count
|
||||
)
|
||||
colors = self._map_edge_average(edge_pixels, segment.edge, segment.led_count)
|
||||
else:
|
||||
colors = self._map_edge_fallback(
|
||||
edge_pixels, segment.edge, segment.led_count
|
||||
)
|
||||
colors = self._map_edge_fallback(edge_pixels, segment.edge, segment.led_count)
|
||||
|
||||
led_array[self._segment_indices[i]] = colors
|
||||
|
||||
# Phase 3: Physical skip — resample full perimeter to active LEDs
|
||||
if self._skip_src is not None:
|
||||
np.copyto(self._skip_float, led_array, casting='unsafe')
|
||||
np.copyto(self._skip_float, led_array, casting="unsafe")
|
||||
for ch in range(3):
|
||||
self._skip_resampled[:, ch] = np.round(
|
||||
np.interp(self._skip_src, self._skip_x, self._skip_float[:, ch])
|
||||
).astype(np.uint8)
|
||||
led_array[:] = 0
|
||||
end_idx = self._total_leds - self._skip_end
|
||||
led_array[self._skip_start:end_idx] = self._skip_resampled
|
||||
led_array[self._skip_start : end_idx] = self._skip_resampled
|
||||
elif self._active_count <= 0:
|
||||
led_array[:] = 0
|
||||
|
||||
return led_array
|
||||
|
||||
def test_calibration(self, edge: str, color: Tuple[int, int, int]) -> List[Tuple[int, int, int]]:
|
||||
def test_calibration(
|
||||
self, edge: str, color: Tuple[int, int, int]
|
||||
) -> List[Tuple[int, int, int]]:
|
||||
"""Generate test pattern to light up specific edge.
|
||||
|
||||
Useful for verifying calibration configuration.
|
||||
@@ -525,8 +538,11 @@ class AdvancedPixelMapper:
|
||||
|
||||
@staticmethod
|
||||
def _extract_edge_strip(
|
||||
frame: np.ndarray, edge: str, border_width: int,
|
||||
span_start: float, span_end: float,
|
||||
frame: np.ndarray,
|
||||
edge: str,
|
||||
border_width: int,
|
||||
span_start: float,
|
||||
span_end: float,
|
||||
) -> np.ndarray:
|
||||
"""Extract a border strip from a frame for the given edge and span."""
|
||||
h, w = frame.shape[:2]
|
||||
@@ -555,7 +571,10 @@ class AdvancedPixelMapper:
|
||||
return strip
|
||||
|
||||
def _map_edge_average(
|
||||
self, edge_pixels: np.ndarray, edge_name: str, led_count: int,
|
||||
self,
|
||||
edge_pixels: np.ndarray,
|
||||
edge_name: str,
|
||||
led_count: int,
|
||||
cache_key: int,
|
||||
) -> np.ndarray:
|
||||
"""Vectorized average-color mapping (same algo as PixelMapper)."""
|
||||
@@ -588,7 +607,10 @@ class AdvancedPixelMapper:
|
||||
return np.clip(segment_sums / lengths, 0, 255).astype(np.uint8)
|
||||
|
||||
def _map_edge_fallback(
|
||||
self, edge_pixels: np.ndarray, edge_name: str, led_count: int,
|
||||
self,
|
||||
edge_pixels: np.ndarray,
|
||||
edge_name: str,
|
||||
led_count: int,
|
||||
) -> np.ndarray:
|
||||
"""Per-LED color mapping for median/dominant modes."""
|
||||
if edge_name in ("top", "bottom"):
|
||||
@@ -627,31 +649,39 @@ class AdvancedPixelMapper:
|
||||
continue
|
||||
|
||||
edge_pixels = self._extract_edge_strip(
|
||||
frame, line.edge, line.border_width,
|
||||
line.span_start, line.span_end,
|
||||
frame,
|
||||
line.edge,
|
||||
line.border_width,
|
||||
line.span_start,
|
||||
line.span_end,
|
||||
)
|
||||
|
||||
if self._use_fast_avg:
|
||||
colors = self._map_edge_average(
|
||||
edge_pixels, line.edge, line.led_count, cache_key=i,
|
||||
edge_pixels,
|
||||
line.edge,
|
||||
line.led_count,
|
||||
cache_key=i,
|
||||
)
|
||||
else:
|
||||
colors = self._map_edge_fallback(
|
||||
edge_pixels, line.edge, line.led_count,
|
||||
edge_pixels,
|
||||
line.edge,
|
||||
line.led_count,
|
||||
)
|
||||
|
||||
led_array[self._line_indices[i]] = colors
|
||||
|
||||
# Phase 3: Physical skip (same as PixelMapper)
|
||||
if self._skip_src is not None:
|
||||
np.copyto(self._skip_float, led_array, casting='unsafe')
|
||||
np.copyto(self._skip_float, led_array, casting="unsafe")
|
||||
for ch in range(3):
|
||||
self._skip_resampled[:, ch] = np.round(
|
||||
np.interp(self._skip_src, self._skip_x, self._skip_float[:, ch])
|
||||
).astype(np.uint8)
|
||||
led_array[:] = 0
|
||||
end_idx = self._total_leds - self._skip_end
|
||||
led_array[self._skip_start:end_idx] = self._skip_resampled
|
||||
led_array[self._skip_start : end_idx] = self._skip_resampled
|
||||
elif self._active_count <= 0:
|
||||
led_array[:] = 0
|
||||
|
||||
@@ -691,7 +721,7 @@ def create_default_calibration(
|
||||
|
||||
# Distribute LEDs proportionally to aspect ratio (same density per edge)
|
||||
perimeter = 2 * (aspect_width + aspect_height)
|
||||
h_frac = aspect_width / perimeter # fraction for each horizontal edge
|
||||
h_frac = aspect_width / perimeter # fraction for each horizontal edge
|
||||
v_frac = aspect_height / perimeter # fraction for each vertical edge
|
||||
|
||||
# Float counts, then round so total == led_count
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
from typing import List, Tuple, Union
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.utils import get_logger
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
+5
-9
@@ -6,7 +6,7 @@ from typing import List
|
||||
import mss
|
||||
import numpy as np
|
||||
|
||||
from wled_controller.utils import get_logger, get_monitor_names, get_monitor_refresh_rates
|
||||
from ledgrab.utils import get_logger, get_monitor_names, get_monitor_refresh_rates
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -123,7 +123,8 @@ def capture_display(display_index: int = 0) -> ScreenCapture:
|
||||
|
||||
# Direct bytes→numpy (skips PIL intermediate object)
|
||||
img_array = np.frombuffer(
|
||||
screenshot.rgb, dtype=np.uint8,
|
||||
screenshot.rgb,
|
||||
dtype=np.uint8,
|
||||
).reshape(screenshot.height, screenshot.width, 3)
|
||||
|
||||
logger.debug(
|
||||
@@ -144,10 +145,7 @@ def capture_display(display_index: int = 0) -> ScreenCapture:
|
||||
raise RuntimeError(f"Screen capture failed: {e}")
|
||||
|
||||
|
||||
def extract_border_pixels(
|
||||
screen_capture: ScreenCapture,
|
||||
border_width: int = 10
|
||||
) -> BorderPixels:
|
||||
def extract_border_pixels(screen_capture: ScreenCapture, border_width: int = 10) -> BorderPixels:
|
||||
"""Extract border pixels from screen capture.
|
||||
|
||||
Args:
|
||||
@@ -199,9 +197,7 @@ def extract_border_pixels(
|
||||
|
||||
|
||||
def get_edge_segments(
|
||||
edge_pixels: np.ndarray,
|
||||
segment_count: int,
|
||||
edge_name: str
|
||||
edge_pixels: np.ndarray, segment_count: int, edge_name: str
|
||||
) -> List[np.ndarray]:
|
||||
"""Divide edge pixels into segments.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user