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