Files
thomas 1f110866f5
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
Update docs and screenshots
2026-07-25 10:26:29 +02:00

153 lines
5.6 KiB
Python

from __future__ import annotations
from datetime import UTC, datetime
from typing import Literal
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.core.config import get_settings
from app.core.database import SessionLocal
from app.dmx.backends import (
ArtNetDmxBackend,
ArtNetNode,
OlaDmxBackend,
SimulatorDmxBackend,
discover_artnet_nodes,
)
from app.dmx.engine import DmxEngine
from app.models.entities import Setting
DMX_OUTPUT_SETTING_KEY = "dmx_output"
BackendKind = Literal["simulator", "ola", "artnet"]
class DmxOutputService:
def __init__(
self,
engine: DmxEngine,
session_factory: async_sessionmaker[AsyncSession] = SessionLocal,
) -> None:
self.engine = engine
self._session_factory = session_factory
self._settings = get_settings()
self._config = self._default_config()
self._artnet_nodes: list[ArtNetNode] = []
async def startup(self) -> None:
await self._load_config()
self.engine.configure_backend(self._build_backend(self._config))
async def get_config(self) -> dict[str, object]:
return {
**self._config,
"artnet_nodes": [self._serialize_node(node) for node in self._artnet_nodes],
}
async def save_config(self, payload: dict[str, object]) -> dict[str, object]:
normalized = self._normalize_config(payload)
self._config = normalized
await self._persist_config(normalized)
backend = self._build_backend(normalized)
if self.engine.is_running:
await self.engine.replace_backend(backend)
else:
self.engine.configure_backend(backend)
return await self.get_config()
async def discover_artnet(self, timeout_s: float = 1.0) -> dict[str, object]:
nodes = await self._discover(timeout_s)
self._artnet_nodes = nodes
return {
"items": [self._serialize_node(node) for node in nodes],
"count": len(nodes),
}
async def _discover(self, timeout_s: float) -> list[ArtNetNode]:
return await self._to_thread_discovery(timeout_s)
async def _to_thread_discovery(self, timeout_s: float) -> list[ArtNetNode]:
import asyncio
return await asyncio.to_thread(discover_artnet_nodes, timeout_s)
async def _load_config(self) -> None:
async with self._session_factory() as session:
setting = await session.get(Setting, DMX_OUTPUT_SETTING_KEY)
if setting is None or not isinstance(setting.value, dict):
return
self._config = self._normalize_config(setting.value)
async def _persist_config(self, config: dict[str, object]) -> None:
async with self._session_factory() as session:
setting = await session.get(Setting, DMX_OUTPUT_SETTING_KEY)
if setting is None:
setting = Setting(
key=DMX_OUTPUT_SETTING_KEY,
value=config,
updated_at=datetime.now(UTC),
)
session.add(setting)
else:
setting.value = config
setting.updated_at = datetime.now(UTC)
await session.commit()
def _default_config(self) -> dict[str, object]:
backend: BackendKind = "simulator" if self._settings.simulator_enabled else "ola"
return {
"backend": backend,
"universe": self._settings.ola_universe,
"output_port": self._settings.ola_output_port or "",
"target_host": "",
}
def _normalize_config(self, payload: dict[str, object]) -> dict[str, object]:
backend = str(payload.get("backend", self._default_config()["backend"])).lower()
if backend not in {"simulator", "ola", "artnet"}:
backend = "simulator"
universe = int(payload.get("universe", self._settings.ola_universe) or self._settings.ola_universe)
output_port = str(payload.get("output_port", "") or "").strip()
target_host = str(payload.get("target_host", "") or "").strip()
if backend == "artnet" and not target_host:
target_host = "255.255.255.255"
if backend == "simulator":
output_port = "simulator"
return {
"backend": backend,
"universe": max(1, min(63999, universe)),
"output_port": output_port,
"target_host": target_host,
}
def _build_backend(self, config: dict[str, object]):
backend = str(config["backend"])
universe = int(config["universe"])
output_port = str(config.get("output_port", "") or "") or None
if backend == "ola":
return OlaDmxBackend(
universe=universe,
output_port=output_port,
send_timeout_s=self._settings.ola_send_timeout_ms / 1000,
)
if backend == "artnet":
target_host = str(config.get("target_host", "") or "255.255.255.255")
return ArtNetDmxBackend(
universe=universe,
target_host=target_host,
output_port=output_port or target_host,
)
return SimulatorDmxBackend(universe=universe)
def _serialize_node(self, node: ArtNetNode) -> dict[str, object]:
return {
"ip": node.ip,
"short_name": node.short_name,
"long_name": node.long_name,
"label": node.label,
"net": node.net,
"sub_switch": node.sub_switch,
"port_count": node.port_count,
"raw_port_address": node.raw_port_address,
}