Update docs and screenshots
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
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
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
"""Core application services."""
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="TUXDMX_", env_file=".env", extra="ignore")
|
||||
|
||||
app_name: str = "TuxDMX"
|
||||
env: str = "development"
|
||||
secret_key: str = "development-secret-change-me"
|
||||
database_url: str = "sqlite+aiosqlite:///./data/tuxdmx.db"
|
||||
simulator_enabled: bool = True
|
||||
allow_local_docs: bool = True
|
||||
log_level: str = "INFO"
|
||||
target_fps: int = Field(default=30, ge=1, le=44)
|
||||
ola_universe: int = Field(default=1, ge=1, le=63999)
|
||||
ola_output_port: str | None = None
|
||||
ola_send_timeout_ms: int = Field(default=1000, ge=100, le=10000)
|
||||
system_control_enabled: bool = True
|
||||
system_service_name: str = "tuxdmx.service"
|
||||
system_control_helper: str = "/opt/tuxdmx/scripts/control-system.sh"
|
||||
frontend_dist: Path = Path("frontend/dist")
|
||||
data_dir: Path = Path("data")
|
||||
backup_dir: Path = Path("data/backups")
|
||||
fixture_cache_dir: Path = Path("data/fixtures")
|
||||
diagnostics_dir: Path = Path("data/diagnostics")
|
||||
uploads_dir: Path = Path("data/uploads")
|
||||
log_dir: Path = Path("data")
|
||||
|
||||
|
||||
def resolve_path(path: Path) -> Path:
|
||||
return path.expanduser().resolve(strict=False)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_settings() -> Settings:
|
||||
settings = Settings()
|
||||
settings.frontend_dist = resolve_path(settings.frontend_dist)
|
||||
settings.data_dir = resolve_path(settings.data_dir)
|
||||
settings.backup_dir = resolve_path(settings.backup_dir)
|
||||
settings.fixture_cache_dir = resolve_path(settings.fixture_cache_dir)
|
||||
settings.diagnostics_dir = resolve_path(settings.diagnostics_dir)
|
||||
settings.uploads_dir = resolve_path(settings.uploads_dir)
|
||||
settings.log_dir = resolve_path(settings.log_dir)
|
||||
settings.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
settings.backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
settings.fixture_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
settings.diagnostics_dir.mkdir(parents=True, exist_ok=True)
|
||||
settings.uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||
settings.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
return settings
|
||||
|
||||
|
||||
def get_alembic_database_url(database_url: str) -> str:
|
||||
if database_url.startswith("sqlite+aiosqlite:///"):
|
||||
return database_url.replace("sqlite+aiosqlite:///", "sqlite:///", 1)
|
||||
return database_url
|
||||
@@ -0,0 +1,45 @@
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from sqlalchemy import event, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.models.base import Base
|
||||
|
||||
settings = get_settings()
|
||||
engine = create_async_engine(settings.database_url, future=True)
|
||||
SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
|
||||
|
||||
|
||||
@event.listens_for(engine.sync_engine, "connect")
|
||||
def _enable_sqlite_pragmas(dbapi_connection, _connection_record) -> None: # type: ignore[no-untyped-def]
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA journal_mode=WAL;")
|
||||
cursor.execute("PRAGMA foreign_keys=ON;")
|
||||
cursor.close()
|
||||
|
||||
|
||||
async def init_database() -> None:
|
||||
async with engine.begin() as connection:
|
||||
await connection.execute(text("PRAGMA journal_mode=WAL;"))
|
||||
await connection.run_sync(Base.metadata.create_all)
|
||||
await _ensure_compatible_schema(connection)
|
||||
|
||||
|
||||
async def get_db_session() -> AsyncIterator[AsyncSession]:
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
|
||||
|
||||
async def _ensure_compatible_schema(connection) -> None: # type: ignore[no-untyped-def]
|
||||
def sync_ensure(sync_connection) -> None: # type: ignore[no-untyped-def]
|
||||
scene_columns = {
|
||||
row[1]
|
||||
for row in sync_connection.exec_driver_sql("PRAGMA table_info('scenes')").fetchall()
|
||||
}
|
||||
if "targets" not in scene_columns:
|
||||
sync_connection.exec_driver_sql(
|
||||
"ALTER TABLE scenes ADD COLUMN targets JSON NOT NULL DEFAULT '[]'"
|
||||
)
|
||||
|
||||
await connection.run_sync(sync_ensure)
|
||||
@@ -0,0 +1,62 @@
|
||||
from app.auth.service import AuthService
|
||||
from app.backup.service import BackupService
|
||||
from app.bpm.service import BpmService
|
||||
from app.core.config import get_settings
|
||||
from app.dmx.backends import OlaDmxBackend, SimulatorDmxBackend
|
||||
from app.dmx.engine import DmxEngine
|
||||
from app.dmx.output_service import DmxOutputService
|
||||
from app.effects.service import EffectService
|
||||
from app.fixtures.service import FixtureService
|
||||
from app.homeassistant.service import HomeAssistantService
|
||||
from app.live.service import LiveDeskService
|
||||
from app.midi.service import MidiService
|
||||
from app.mixitup.service import TriggerService
|
||||
from app.patch.service import PatchService
|
||||
from app.scenes.service import SceneService
|
||||
from app.system.service import SystemControlService
|
||||
from app.telemetry.service import TelemetryService
|
||||
|
||||
|
||||
class ApplicationState:
|
||||
def __init__(self) -> None:
|
||||
settings = get_settings()
|
||||
self.telemetry = TelemetryService()
|
||||
backend = (
|
||||
SimulatorDmxBackend(universe=settings.ola_universe)
|
||||
if settings.simulator_enabled
|
||||
else OlaDmxBackend(
|
||||
universe=settings.ola_universe,
|
||||
output_port=settings.ola_output_port,
|
||||
send_timeout_s=settings.ola_send_timeout_ms / 1000,
|
||||
)
|
||||
)
|
||||
self.engine = DmxEngine(self.telemetry, backend=backend)
|
||||
self.auth = AuthService()
|
||||
self.fixtures = FixtureService()
|
||||
self.patch = PatchService()
|
||||
self.scenes = SceneService(self.engine)
|
||||
self.bpm = BpmService()
|
||||
self.effects = EffectService(self.engine, self.bpm)
|
||||
self.dmx = DmxOutputService(self.engine)
|
||||
self.home_assistant = HomeAssistantService(self.engine)
|
||||
self.live = LiveDeskService(self.engine, home_assistant=self.home_assistant)
|
||||
self.triggers = TriggerService(self.engine, self.effects, self.scenes)
|
||||
self.midi = MidiService(self.engine, self.scenes, self.effects)
|
||||
self.backups = BackupService(settings.data_dir, settings.backup_dir)
|
||||
self.system = SystemControlService()
|
||||
|
||||
async def startup(self) -> None:
|
||||
await self.dmx.startup()
|
||||
await self.home_assistant.startup()
|
||||
await self.midi.startup()
|
||||
await self.bpm.startup()
|
||||
await self.engine.start()
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
await self.effects.shutdown()
|
||||
await self.home_assistant.shutdown()
|
||||
await self.bpm.shutdown()
|
||||
await self.engine.stop()
|
||||
|
||||
|
||||
app_state = ApplicationState()
|
||||
@@ -0,0 +1,32 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
payload = {
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
return json.dumps(payload, ensure_ascii=True)
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
settings = get_settings()
|
||||
root = logging.getLogger()
|
||||
if root.handlers:
|
||||
return
|
||||
|
||||
root.setLevel(settings.log_level.upper())
|
||||
console = logging.StreamHandler()
|
||||
console.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s"))
|
||||
root.addHandler(console)
|
||||
|
||||
json_handler = logging.FileHandler(settings.log_dir / "tuxdmx.json.log", encoding="utf-8")
|
||||
json_handler.setFormatter(JsonFormatter())
|
||||
root.addHandler(json_handler)
|
||||
Reference in New Issue
Block a user