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 @@
|
||||
"""ORM and schema modules."""
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import JSON, Boolean, DateTime, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
return datetime.now(UTC)
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
||||
password_hash: Mapped[str] = mapped_column(String(255))
|
||||
role: Mapped[str] = mapped_column(String(30), default="administrator")
|
||||
disabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class SessionRecord(Base):
|
||||
__tablename__ = "sessions"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, index=True)
|
||||
csrf_token: Mapped[str] = mapped_column(String(64))
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class ApiToken(Base):
|
||||
__tablename__ = "api_tokens"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
label: Mapped[str] = mapped_column(String(120))
|
||||
token_hash: Mapped[str] = mapped_column(String(255))
|
||||
role: Mapped[str] = mapped_column(String(30), default="operator")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class Setting(Base):
|
||||
__tablename__ = "settings"
|
||||
|
||||
key: Mapped[str] = mapped_column(String(120), primary_key=True)
|
||||
value: Mapped[dict[str, object]] = mapped_column(JSON, default=dict)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class FixtureSource(Base):
|
||||
__tablename__ = "fixture_sources"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
manufacturer_key: Mapped[str] = mapped_column(String(100))
|
||||
fixture_key: Mapped[str] = mapped_column(String(150))
|
||||
schema_ref: Mapped[str] = mapped_column(String(255))
|
||||
source_url: Mapped[str] = mapped_column(String(255))
|
||||
payload: Mapped[dict[str, object]] = mapped_column(JSON)
|
||||
payload_hash: Mapped[str] = mapped_column(String(64))
|
||||
imported_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class FixtureDefinition(Base):
|
||||
__tablename__ = "fixture_definitions"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
slug: Mapped[str] = mapped_column(String(160), unique=True, index=True)
|
||||
manufacturer: Mapped[str] = mapped_column(String(120))
|
||||
model: Mapped[str] = mapped_column(String(160))
|
||||
short_name: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
||||
categories: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
normalized_data: Mapped[dict[str, object]] = mapped_column(JSON)
|
||||
source_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class FixtureInstance(Base):
|
||||
__tablename__ = "fixture_instances"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
universe: Mapped[int] = mapped_column(Integer, default=1)
|
||||
name: Mapped[str] = mapped_column(String(160))
|
||||
definition_id: Mapped[int] = mapped_column(Integer, index=True)
|
||||
mode_key: Mapped[str] = mapped_column(String(120))
|
||||
start_address: Mapped[int] = mapped_column(Integer)
|
||||
channel_count: Mapped[int] = mapped_column(Integer)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
group_names: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
position: Mapped[dict[str, object]] = mapped_column(JSON, default=dict)
|
||||
tags: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class Scene(Base):
|
||||
__tablename__ = "scenes"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(160))
|
||||
slug: Mapped[str] = mapped_column(String(160), unique=True, index=True)
|
||||
color: Mapped[str] = mapped_column(String(20), default="#00e5ff")
|
||||
icon: Mapped[str | None] = mapped_column(String(40), nullable=True)
|
||||
priority: Mapped[int] = mapped_column(Integer, default=10)
|
||||
fade_in_ms: Mapped[int] = mapped_column(Integer, default=500)
|
||||
fade_out_ms: Mapped[int] = mapped_column(Integer, default=500)
|
||||
hold_ms: Mapped[int] = mapped_column(Integer, default=0)
|
||||
master_limit: Mapped[int] = mapped_column(Integer, default=255)
|
||||
values: Mapped[list[dict[str, object]]] = mapped_column(JSON, default=list)
|
||||
targets: Mapped[list[dict[str, object]]] = mapped_column(JSON, default=list)
|
||||
tags: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class Effect(Base):
|
||||
__tablename__ = "effects"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(160))
|
||||
slug: Mapped[str] = mapped_column(String(160), unique=True, index=True)
|
||||
effect_type: Mapped[str] = mapped_column(String(80))
|
||||
parameters: Mapped[dict[str, object]] = mapped_column(JSON, default=dict)
|
||||
priority: Mapped[int] = mapped_column(Integer, default=40)
|
||||
cooldown_ms: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class TriggerMapping(Base):
|
||||
__tablename__ = "trigger_mappings"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
slug: Mapped[str] = mapped_column(String(160), unique=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String(160))
|
||||
event_type: Mapped[str] = mapped_column(String(60))
|
||||
action_type: Mapped[str] = mapped_column(String(30))
|
||||
action_slug: Mapped[str] = mapped_column(String(160))
|
||||
config: Mapped[dict[str, object]] = mapped_column(JSON, default=dict)
|
||||
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class TriggerEvent(Base):
|
||||
__tablename__ = "trigger_events"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
slug: Mapped[str] = mapped_column(String(160), index=True)
|
||||
event_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(30), default="queued")
|
||||
payload: Mapped[dict[str, object]] = mapped_column(JSON, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class SystemEvent(Base):
|
||||
__tablename__ = "system_events"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
category: Mapped[str] = mapped_column(String(40))
|
||||
level: Mapped[str] = mapped_column(String(20))
|
||||
message: Mapped[str] = mapped_column(Text)
|
||||
payload: Mapped[dict[str, object]] = mapped_column(JSON, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class BackupRecord(Base):
|
||||
__tablename__ = "backups"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
label: Mapped[str] = mapped_column(String(160))
|
||||
path: Mapped[str] = mapped_column(String(255))
|
||||
manifest: Mapped[dict[str, object]] = mapped_column(JSON, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class MidiBridge(Base):
|
||||
__tablename__ = "midi_bridges"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
bridge_id: Mapped[str] = mapped_column(String(160), unique=True, index=True)
|
||||
device_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
ip_address: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
protocol_version: Mapped[int] = mapped_column(Integer, default=1)
|
||||
online: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
last_heartbeat_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
last_event_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
last_event: Mapped[dict[str, object]] = mapped_column(JSON, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class MidiBridgeToken(Base):
|
||||
__tablename__ = "midi_bridge_tokens"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
label: Mapped[str] = mapped_column(String(120))
|
||||
token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
bridge_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
||||
scopes: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
|
||||
|
||||
class MidiMapping(Base):
|
||||
__tablename__ = "midi_mappings"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(160))
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
bridge_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
||||
device_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
message_type: Mapped[str] = mapped_column(String(40))
|
||||
channel: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
number: Mapped[int] = mapped_column(Integer)
|
||||
action: Mapped[str] = mapped_column(String(60))
|
||||
target_type: Mapped[str] = mapped_column(String(40))
|
||||
target_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
||||
mode: Mapped[str] = mapped_column(String(40), default="trigger")
|
||||
minimum_value: Mapped[int] = mapped_column(Integer, default=0)
|
||||
maximum_value: Mapped[int] = mapped_column(Integer, default=127)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||
@@ -0,0 +1,227 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
status: str
|
||||
simulator_enabled: bool
|
||||
version: str
|
||||
|
||||
|
||||
class ChannelValue(BaseModel):
|
||||
channel: int = Field(ge=1, le=512)
|
||||
value: int = Field(ge=0, le=255)
|
||||
precedence: str = "htp"
|
||||
source: str = "scene"
|
||||
|
||||
|
||||
class FixturePositionPayload(BaseModel):
|
||||
x: float = Field(default=50.0, ge=0.0, le=100.0)
|
||||
y: float = Field(default=50.0, ge=0.0, le=100.0)
|
||||
z: float = Field(default=0.0, ge=0.0, le=100.0)
|
||||
rotation: float = Field(default=0.0, ge=0.0, le=360.0)
|
||||
|
||||
|
||||
class SceneAttributeValue(BaseModel):
|
||||
attribute: str
|
||||
value: int = Field(ge=0, le=255)
|
||||
precedence: str | None = None
|
||||
|
||||
|
||||
class SceneTargetPayload(BaseModel):
|
||||
target_type: Literal["fixture", "group"]
|
||||
patch_id: int | None = Field(default=None, ge=1)
|
||||
group_name: str | None = None
|
||||
values: list[SceneAttributeValue] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ScenePayload(BaseModel):
|
||||
name: str
|
||||
slug: str
|
||||
color: str = "#00e5ff"
|
||||
icon: str | None = None
|
||||
priority: int = 10
|
||||
fade_in_ms: int = 500
|
||||
fade_out_ms: int = 500
|
||||
hold_ms: int = 0
|
||||
master_limit: int = 255
|
||||
values: list[ChannelValue] = Field(default_factory=list)
|
||||
targets: list[SceneTargetPayload] = Field(default_factory=list)
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class EffectPayload(BaseModel):
|
||||
name: str
|
||||
slug: str
|
||||
effect_type: str
|
||||
parameters: dict[str, object] = Field(default_factory=dict)
|
||||
priority: int = 40
|
||||
cooldown_ms: int = 0
|
||||
|
||||
|
||||
class TriggerRequest(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
event_type: str = Field(default="custom", alias="eventType")
|
||||
username: str | None = None
|
||||
display_name: str | None = Field(default=None, alias="displayName")
|
||||
amount: int | None = None
|
||||
event_id: str | None = Field(default=None, alias="eventId")
|
||||
platform: str = "twitch"
|
||||
metadata: dict[str, object] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class TelemetrySnapshot(BaseModel):
|
||||
uptime_seconds: float
|
||||
cpu_percent: float
|
||||
ram_percent: float
|
||||
temperature_c: float | None = None
|
||||
queue_depth: int
|
||||
reconnect_count: int
|
||||
last_error: str | None = None
|
||||
last_sent_at: datetime | None = None
|
||||
|
||||
|
||||
class PatchPayload(BaseModel):
|
||||
universe: int = Field(default=1, ge=1, le=63999)
|
||||
name: str
|
||||
definition_id: int = Field(ge=1)
|
||||
mode_key: str
|
||||
start_address: int = Field(ge=1, le=512)
|
||||
enabled: bool = True
|
||||
group_names: list[str] = Field(default_factory=list)
|
||||
position: FixturePositionPayload = Field(default_factory=FixturePositionPayload)
|
||||
|
||||
|
||||
class PatchValidationPayload(BaseModel):
|
||||
universe: int = Field(default=1, ge=1, le=63999)
|
||||
definition_id: int = Field(ge=1)
|
||||
mode_key: str
|
||||
start_address: int = Field(ge=1, le=512)
|
||||
enabled: bool = True
|
||||
exclude_patch_id: int | None = Field(default=None, ge=1)
|
||||
|
||||
|
||||
class ManualPatchValuesPayload(BaseModel):
|
||||
values: dict[int, int] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class BpmAudioStartPayload(BaseModel):
|
||||
device: str = "alsa:auto"
|
||||
|
||||
|
||||
class BpmAudioConfigPayload(BaseModel):
|
||||
preferred_device: str = "alsa:auto"
|
||||
|
||||
|
||||
class DmxOutputConfigPayload(BaseModel):
|
||||
backend: Literal["simulator", "ola", "artnet"] = "simulator"
|
||||
universe: int = Field(default=1, ge=1, le=63999)
|
||||
output_port: str | None = None
|
||||
target_host: str | None = None
|
||||
|
||||
|
||||
class HomeAssistantConfigPayload(BaseModel):
|
||||
enabled: bool = False
|
||||
base_url: str = ""
|
||||
token: str | None = None
|
||||
default_universe: int = Field(default=10, ge=1, le=63999)
|
||||
|
||||
|
||||
class HomeAssistantMappingPayload(BaseModel):
|
||||
name: str
|
||||
universe: int = Field(default=10, ge=1, le=63999)
|
||||
start_address: int = Field(ge=1, le=512)
|
||||
fixture_type: Literal["dimmer", "rgb", "rgbw", "cct", "switch", "scene", "automation"] = "dimmer"
|
||||
entity_id: str
|
||||
rate_limit_hz: float = Field(default=5.0, ge=0.1, le=30.0)
|
||||
deadband: int = Field(default=2, ge=0, le=255)
|
||||
fade_ms: int = Field(default=0, ge=0, le=10000)
|
||||
invert_channel: bool = False
|
||||
min_value: int = Field(default=0, ge=0, le=255)
|
||||
max_value: int = Field(default=255, ge=0, le=255)
|
||||
enabled: bool = True
|
||||
master_dimmer: bool = True
|
||||
|
||||
|
||||
class MidiBridgeTokenCreatePayload(BaseModel):
|
||||
label: str
|
||||
bridge_id: str | None = None
|
||||
scopes: list[str] = Field(default_factory=lambda: ["midi:connect", "midi:events", "midi:heartbeat"])
|
||||
|
||||
|
||||
class MidiBridgeTokenResponse(BaseModel):
|
||||
id: int
|
||||
label: str
|
||||
bridge_id: str | None
|
||||
scopes: list[str]
|
||||
created_at: datetime
|
||||
revoked_at: datetime | None = None
|
||||
last_used_at: datetime | None = None
|
||||
token_preview: str
|
||||
|
||||
|
||||
class MidiMessagePayload(BaseModel):
|
||||
type: Literal["note_on", "note_off", "control_change", "program_change"]
|
||||
channel: int = Field(ge=0, le=15)
|
||||
number: int = Field(ge=0, le=127)
|
||||
value: int = Field(ge=0, le=127)
|
||||
|
||||
|
||||
class MidiBridgeEventPayload(BaseModel):
|
||||
type: Literal["midi_event"]
|
||||
protocol_version: int = Field(default=1, ge=1)
|
||||
bridge_id: str
|
||||
device: str
|
||||
timestamp: datetime
|
||||
message: MidiMessagePayload
|
||||
|
||||
|
||||
class MidiBridgeHeartbeatPayload(BaseModel):
|
||||
type: Literal["heartbeat"]
|
||||
protocol_version: int = Field(default=1, ge=1)
|
||||
bridge_id: str
|
||||
device: str | None = None
|
||||
timestamp: datetime | None = None
|
||||
|
||||
|
||||
class MidiMappingPayload(BaseModel):
|
||||
name: str
|
||||
enabled: bool = True
|
||||
bridge_id: str | None = None
|
||||
device_name: str | None = None
|
||||
message_type: Literal["note_on", "note_off", "control_change", "program_change"]
|
||||
channel: int | None = Field(default=None, ge=0, le=15)
|
||||
number: int = Field(ge=0, le=127)
|
||||
action: Literal[
|
||||
"activate_scene",
|
||||
"deactivate_scene",
|
||||
"toggle_scene",
|
||||
"flash_scene",
|
||||
"blackout_on",
|
||||
"blackout_off",
|
||||
"blackout_toggle",
|
||||
"set_master_dimmer",
|
||||
"set_scene_intensity",
|
||||
"start_effect",
|
||||
"stop_effect",
|
||||
]
|
||||
target_type: Literal["scene", "effect", "system", "global"] = "scene"
|
||||
target_id: str | None = None
|
||||
mode: Literal["trigger", "toggle", "hold", "flash", "continuous"] = "trigger"
|
||||
minimum_value: int = Field(default=0, ge=0, le=127)
|
||||
maximum_value: int = Field(default=127, ge=0, le=127)
|
||||
|
||||
|
||||
class MidiLearnStartPayload(BaseModel):
|
||||
timeout_seconds: int = Field(default=15, ge=3, le=120)
|
||||
allow_passthrough: bool = False
|
||||
|
||||
|
||||
class MidiTestMappingPayload(BaseModel):
|
||||
value: int = Field(default=127, ge=0, le=127)
|
||||
message_type: Literal["note_on", "note_off", "control_change", "program_change"] | None = None
|
||||
Reference in New Issue
Block a user