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 @@
|
||||
"""API modules."""
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routes import router
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(router)
|
||||
|
||||
@@ -0,0 +1,702 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.dependencies import app_state
|
||||
from app.models.schemas import (
|
||||
BpmAudioConfigPayload,
|
||||
BpmAudioStartPayload,
|
||||
ChannelValue,
|
||||
DmxOutputConfigPayload,
|
||||
EffectPayload,
|
||||
HealthResponse,
|
||||
HomeAssistantConfigPayload,
|
||||
HomeAssistantMappingPayload,
|
||||
MidiBridgeEventPayload,
|
||||
MidiBridgeHeartbeatPayload,
|
||||
MidiBridgeTokenCreatePayload,
|
||||
MidiLearnStartPayload,
|
||||
MidiMappingPayload,
|
||||
MidiTestMappingPayload,
|
||||
ManualPatchValuesPayload,
|
||||
PatchPayload,
|
||||
PatchValidationPayload,
|
||||
ScenePayload,
|
||||
TriggerRequest,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def _extract_bearer_token(request: Request) -> str | None:
|
||||
authorization = request.headers.get("Authorization", "")
|
||||
if not authorization.lower().startswith("bearer "):
|
||||
return None
|
||||
return authorization[7:].strip() or None
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
async def health() -> HealthResponse:
|
||||
return HealthResponse(
|
||||
status="ok",
|
||||
simulator_enabled=settings.simulator_enabled,
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/system/status")
|
||||
async def system_status() -> dict[str, object]:
|
||||
return {
|
||||
"app": settings.app_name,
|
||||
"setup_required": False,
|
||||
"engine": app_state.engine.snapshot(),
|
||||
"telemetry": app_state.telemetry.snapshot(len(app_state.triggers.queue)),
|
||||
"bpm": app_state.bpm.snapshot(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/system/diagnostics")
|
||||
async def diagnostics() -> dict[str, object]:
|
||||
return {
|
||||
"engine": app_state.engine.snapshot(),
|
||||
"telemetry": app_state.telemetry.snapshot(len(app_state.triggers.queue)),
|
||||
"trigger_queue": app_state.triggers.snapshot(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/system/restart-service")
|
||||
async def restart_service() -> dict[str, object]:
|
||||
return await app_state.system.restart_service()
|
||||
|
||||
|
||||
@router.post("/system/reboot-host")
|
||||
async def reboot_host() -> dict[str, object]:
|
||||
return await app_state.system.reboot_host()
|
||||
|
||||
|
||||
@router.get("/dmx/status")
|
||||
async def dmx_status() -> dict[str, object]:
|
||||
return app_state.engine.snapshot()
|
||||
|
||||
|
||||
@router.get("/dmx/devices")
|
||||
async def dmx_devices() -> dict[str, object]:
|
||||
status = app_state.engine.backend.get_status()
|
||||
return {
|
||||
"devices": [
|
||||
{
|
||||
"name": status.device_name,
|
||||
"backend": status.backend_name,
|
||||
"connected": status.connected,
|
||||
"output_port": status.selected_output_port,
|
||||
"universe": status.selected_universe,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/dmx/config")
|
||||
async def dmx_config() -> dict[str, object]:
|
||||
return await app_state.dmx.get_config()
|
||||
|
||||
|
||||
@router.put("/dmx/config")
|
||||
async def dmx_config_save(payload: DmxOutputConfigPayload) -> dict[str, object]:
|
||||
return await app_state.dmx.save_config(payload.model_dump())
|
||||
|
||||
|
||||
@router.post("/dmx/artnet/discover")
|
||||
async def dmx_artnet_discover(timeout_s: float = 1.0) -> dict[str, object]:
|
||||
return await app_state.dmx.discover_artnet(timeout_s)
|
||||
|
||||
|
||||
@router.get("/dmx/universes")
|
||||
async def dmx_universes() -> dict[str, object]:
|
||||
status = app_state.engine.backend.get_status()
|
||||
return {
|
||||
"universes": [
|
||||
{
|
||||
"id": status.selected_universe,
|
||||
"frame_rate": settings.target_fps,
|
||||
"output_port": status.selected_output_port,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/dmx/frame")
|
||||
async def dmx_frame(universe: int | None = None) -> dict[str, object]:
|
||||
selected_universe = universe or app_state.engine.backend.get_status().selected_universe
|
||||
frame = app_state.engine.get_frame(selected_universe)
|
||||
return {
|
||||
"universe": frame.universe,
|
||||
"values": frame.values,
|
||||
"sources": frame.source_map,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/dmx/test-channel")
|
||||
async def dmx_test_channel(channel: int, value: int) -> dict[str, object]:
|
||||
payload = ScenePayload(
|
||||
name=f"Test kanal {channel}",
|
||||
slug=f"test-channel-{channel}",
|
||||
values=[ChannelValue(channel=channel, value=value, precedence="htp", source="test")],
|
||||
)
|
||||
await app_state.scenes.save(payload)
|
||||
await app_state.scenes.activate(payload.slug)
|
||||
return {"status": "queued", "scene": payload}
|
||||
|
||||
|
||||
@router.post("/dmx/blackout")
|
||||
async def dmx_blackout() -> dict[str, str]:
|
||||
app_state.engine.trigger_blackout()
|
||||
return {"status": "blackout-active"}
|
||||
|
||||
|
||||
@router.post("/dmx/release-blackout")
|
||||
async def dmx_release_blackout() -> dict[str, str]:
|
||||
app_state.engine.release_blackout()
|
||||
return {"status": "blackout-released"}
|
||||
|
||||
|
||||
@router.get("/live/mixer")
|
||||
async def live_mixer() -> dict[str, object]:
|
||||
return await app_state.live.snapshot()
|
||||
|
||||
|
||||
@router.put("/live/mixer/{patch_id}")
|
||||
async def live_mixer_set(patch_id: int, payload: ManualPatchValuesPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.live.set_patch_values(
|
||||
patch_id,
|
||||
{int(channel): int(value) for channel, value in payload.values.items()},
|
||||
)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/live/mixer/{patch_id}")
|
||||
async def live_mixer_clear(patch_id: int) -> dict[str, object]:
|
||||
return await app_state.live.clear_patch(patch_id)
|
||||
|
||||
|
||||
@router.put("/live/mixer/home-assistant/{mapping_id}")
|
||||
async def live_mixer_set_home_assistant(mapping_id: int, payload: ManualPatchValuesPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.live.set_home_assistant_mapping_values(
|
||||
mapping_id,
|
||||
{int(channel): int(value) for channel, value in payload.values.items()},
|
||||
)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/live/mixer/home-assistant/{mapping_id}")
|
||||
async def live_mixer_clear_home_assistant(mapping_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.live.clear_home_assistant_mapping(mapping_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/fixtures")
|
||||
async def list_fixtures() -> dict[str, object]:
|
||||
return {"items": await app_state.fixtures.list_fixtures()}
|
||||
|
||||
|
||||
@router.post("/fixtures/search-ofl")
|
||||
async def search_ofl(query: str) -> dict[str, object]:
|
||||
return {"results": await app_state.fixtures.search_ofl(query)}
|
||||
|
||||
|
||||
@router.post("/fixtures/import-ofl")
|
||||
async def import_ofl(manufacturer_key: str, fixture_key: str) -> dict[str, object]:
|
||||
return await app_state.fixtures.import_ofl(manufacturer_key, fixture_key)
|
||||
|
||||
|
||||
@router.post("/fixtures/preview-ofl")
|
||||
async def preview_ofl(manufacturer_key: str, fixture_key: str) -> dict[str, object]:
|
||||
return await app_state.fixtures.preview_ofl(manufacturer_key, fixture_key)
|
||||
|
||||
|
||||
@router.post("/fixtures/import-file")
|
||||
async def import_fixture_file(payload: dict[str, object]) -> dict[str, object]:
|
||||
return await app_state.fixtures.import_payload(payload)
|
||||
|
||||
|
||||
@router.post("/fixtures/custom")
|
||||
async def create_custom_fixture(payload: dict[str, object]) -> dict[str, object]:
|
||||
return await import_fixture_file(payload)
|
||||
|
||||
|
||||
@router.get("/fixtures/{fixture_id}")
|
||||
async def get_fixture(fixture_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.fixtures.get_fixture(fixture_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail="Fixture not found") from exc
|
||||
|
||||
|
||||
@router.put("/fixtures/{fixture_id}")
|
||||
async def update_fixture(fixture_id: int, payload: dict[str, object]) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.fixtures.update_fixture(fixture_id, payload)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail="Fixture not found") from exc
|
||||
|
||||
|
||||
@router.delete("/fixtures/{fixture_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_fixture(fixture_id: int) -> Response:
|
||||
try:
|
||||
await app_state.fixtures.delete_fixture(fixture_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail="Fixture not found") from exc
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get("/patch")
|
||||
async def patch_list() -> dict[str, object]:
|
||||
return {"items": await app_state.patch.list_instances()}
|
||||
|
||||
|
||||
@router.post("/patch", status_code=status.HTTP_201_CREATED)
|
||||
async def patch_create(payload: PatchPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.patch.create_instance(payload)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.put("/patch/{patch_id}")
|
||||
async def patch_update(patch_id: int, payload: PatchPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.patch.update_instance(patch_id, payload)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/patch/{patch_id}")
|
||||
async def patch_delete(patch_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.patch.delete_instance(patch_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/patch/validate")
|
||||
async def patch_validate(payload: PatchValidationPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.patch.validate(payload)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/scenes")
|
||||
async def list_scenes() -> dict[str, object]:
|
||||
return {"items": await app_state.scenes.list()}
|
||||
|
||||
|
||||
@router.post("/scenes")
|
||||
async def create_scene(payload: ScenePayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.scenes.save(payload)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/scenes/{scene_id}")
|
||||
async def get_scene(scene_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.scenes.get(scene_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.put("/scenes/{scene_id}")
|
||||
async def update_scene(scene_id: int, payload: ScenePayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.scenes.save(payload, scene_id=scene_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/scenes/{scene_id}")
|
||||
async def delete_scene(scene_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.scenes.delete(scene_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/scenes/{scene_slug}/activate")
|
||||
async def activate_scene(scene_slug: str) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.scenes.activate(scene_slug)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/scenes/{scene_slug}/release")
|
||||
async def release_scene(scene_slug: str) -> dict[str, str]:
|
||||
await app_state.scenes.release(scene_slug)
|
||||
return {"status": "released"}
|
||||
|
||||
|
||||
@router.get("/effects")
|
||||
async def list_effects() -> dict[str, object]:
|
||||
return {"items": [effect.model_dump() for effect in app_state.effects.list()]}
|
||||
|
||||
|
||||
@router.post("/effects")
|
||||
async def create_effect(payload: EffectPayload) -> dict[str, object]:
|
||||
return app_state.effects.save(payload).model_dump()
|
||||
|
||||
|
||||
@router.put("/effects/{effect_id}")
|
||||
async def update_effect(effect_id: int, payload: EffectPayload) -> dict[str, object]:
|
||||
return {"id": effect_id, **app_state.effects.save(payload).model_dump()}
|
||||
|
||||
|
||||
@router.delete("/effects/{effect_id}")
|
||||
async def delete_effect(effect_id: int) -> dict[str, object]:
|
||||
effects = app_state.effects.list()
|
||||
if effect_id >= len(effects):
|
||||
raise HTTPException(status_code=404, detail="Effect not found")
|
||||
slug = effects[effect_id].slug
|
||||
app_state.effects.effects.pop(slug, None)
|
||||
return {"deleted": slug}
|
||||
|
||||
|
||||
@router.post("/effects/{effect_slug}/trigger")
|
||||
async def trigger_effect(effect_slug: str) -> dict[str, object]:
|
||||
return app_state.effects.trigger(effect_slug).model_dump()
|
||||
|
||||
|
||||
@router.post("/effects/{effect_slug}/stop")
|
||||
async def stop_effect(effect_slug: str) -> dict[str, str]:
|
||||
app_state.effects.stop(effect_slug)
|
||||
return {"status": "stopped"}
|
||||
|
||||
|
||||
@router.get("/bpm/status")
|
||||
async def bpm_status() -> dict[str, object]:
|
||||
return app_state.bpm.snapshot()
|
||||
|
||||
|
||||
@router.post("/bpm/manual")
|
||||
async def bpm_manual(bpm: float) -> dict[str, object]:
|
||||
return app_state.bpm.set_manual(bpm)
|
||||
|
||||
|
||||
@router.post("/bpm/tap")
|
||||
async def bpm_tap() -> dict[str, object]:
|
||||
return app_state.bpm.tap()
|
||||
|
||||
|
||||
@router.post("/bpm/audio/start")
|
||||
async def bpm_audio_start(payload: BpmAudioStartPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.bpm.start_audio(payload.device)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/bpm/audio/stop")
|
||||
async def bpm_audio_stop() -> dict[str, str]:
|
||||
snapshot = await app_state.bpm.stop_audio()
|
||||
return {"status": "stopped", "mode": str(snapshot["mode"])}
|
||||
|
||||
|
||||
@router.get("/bpm/config")
|
||||
async def bpm_config() -> dict[str, object]:
|
||||
return await app_state.bpm.get_config()
|
||||
|
||||
|
||||
@router.put("/bpm/config")
|
||||
async def bpm_config_save(payload: BpmAudioConfigPayload) -> dict[str, object]:
|
||||
return await app_state.bpm.save_config(payload.preferred_device)
|
||||
|
||||
|
||||
@router.post("/bpm/external")
|
||||
async def bpm_external(bpm: float) -> dict[str, object]:
|
||||
return app_state.bpm.set_manual(bpm)
|
||||
|
||||
|
||||
@router.get("/bpm/devices")
|
||||
async def bpm_devices() -> dict[str, object]:
|
||||
return {"devices": await app_state.bpm.list_devices()}
|
||||
|
||||
|
||||
@router.get("/integrations/mixitup")
|
||||
async def list_mixitup_integrations() -> dict[str, object]:
|
||||
return {"items": []}
|
||||
|
||||
|
||||
@router.post("/integrations/mixitup")
|
||||
async def create_mixitup_integration(payload: dict[str, object]) -> dict[str, object]:
|
||||
return payload
|
||||
|
||||
|
||||
@router.put("/integrations/mixitup/{integration_id}")
|
||||
async def update_mixitup_integration(
|
||||
integration_id: int,
|
||||
payload: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"id": integration_id,
|
||||
**payload,
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/integrations/mixitup/{integration_id}")
|
||||
async def delete_mixitup_integration(integration_id: int) -> dict[str, object]:
|
||||
return {"deleted": integration_id}
|
||||
|
||||
|
||||
@router.post("/integrations/mixitup/{integration_id}/test")
|
||||
async def test_mixitup_integration(integration_id: int) -> dict[str, object]:
|
||||
return {"id": integration_id, "status": "accepted", "latency_target_ms": 200}
|
||||
|
||||
|
||||
@router.get("/integrations/home-assistant/config")
|
||||
async def home_assistant_config() -> dict[str, object]:
|
||||
return await app_state.home_assistant.get_config()
|
||||
|
||||
|
||||
@router.put("/integrations/home-assistant/config")
|
||||
async def save_home_assistant_config(payload: HomeAssistantConfigPayload) -> dict[str, object]:
|
||||
return await app_state.home_assistant.save_config(payload.model_dump())
|
||||
|
||||
|
||||
@router.post("/integrations/home-assistant/test-connection")
|
||||
async def test_home_assistant_connection() -> dict[str, object]:
|
||||
return await app_state.home_assistant.test_connection()
|
||||
|
||||
|
||||
@router.get("/integrations/home-assistant/entities")
|
||||
async def list_home_assistant_entities() -> dict[str, object]:
|
||||
return await app_state.home_assistant.list_entities()
|
||||
|
||||
|
||||
@router.get("/integrations/home-assistant/mappings")
|
||||
async def list_home_assistant_mappings() -> dict[str, object]:
|
||||
return await app_state.home_assistant.list_mappings()
|
||||
|
||||
|
||||
@router.post("/integrations/home-assistant/mappings")
|
||||
async def create_home_assistant_mapping(payload: HomeAssistantMappingPayload) -> dict[str, object]:
|
||||
return await app_state.home_assistant.create_mapping(payload.model_dump())
|
||||
|
||||
|
||||
@router.put("/integrations/home-assistant/mappings/{mapping_id}")
|
||||
async def update_home_assistant_mapping(mapping_id: int, payload: HomeAssistantMappingPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.home_assistant.update_mapping(mapping_id, payload.model_dump())
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/integrations/home-assistant/mappings/{mapping_id}")
|
||||
async def delete_home_assistant_mapping(mapping_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.home_assistant.delete_mapping(mapping_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/integrations/home-assistant/mappings/{mapping_id}/test")
|
||||
async def test_home_assistant_mapping(mapping_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.home_assistant.test_mapping(mapping_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/integrations/midi/bridges")
|
||||
async def list_midi_bridges() -> dict[str, object]:
|
||||
return await app_state.midi.list_bridges()
|
||||
|
||||
|
||||
@router.get("/integrations/midi/mappings")
|
||||
async def list_midi_mappings() -> dict[str, object]:
|
||||
return await app_state.midi.list_mappings()
|
||||
|
||||
|
||||
@router.post("/integrations/midi/mappings")
|
||||
async def create_midi_mapping(payload: MidiMappingPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.midi.create_mapping(payload.model_dump())
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.put("/integrations/midi/mappings/{mapping_id}")
|
||||
async def update_midi_mapping(mapping_id: int, payload: MidiMappingPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.midi.update_mapping(mapping_id, payload.model_dump())
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/integrations/midi/mappings/{mapping_id}")
|
||||
async def delete_midi_mapping(mapping_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.midi.delete_mapping(mapping_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/integrations/midi/mappings/{mapping_id}/test")
|
||||
async def test_midi_mapping(mapping_id: int, payload: MidiTestMappingPayload) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.midi.test_mapping(mapping_id, payload.value, payload.message_type)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/integrations/midi/tokens")
|
||||
async def list_midi_tokens() -> dict[str, object]:
|
||||
return await app_state.midi.list_tokens()
|
||||
|
||||
|
||||
@router.post("/integrations/midi/tokens")
|
||||
async def create_midi_token(payload: MidiBridgeTokenCreatePayload) -> dict[str, object]:
|
||||
return await app_state.midi.create_token(payload.model_dump())
|
||||
|
||||
|
||||
@router.delete("/integrations/midi/tokens/{token_id}")
|
||||
async def revoke_midi_token(token_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return await app_state.midi.revoke_token(token_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/integrations/midi/learn")
|
||||
async def get_midi_learn_state() -> dict[str, object]:
|
||||
return await app_state.midi.get_learn_state()
|
||||
|
||||
|
||||
@router.post("/integrations/midi/learn/start")
|
||||
async def start_midi_learn(payload: MidiLearnStartPayload) -> dict[str, object]:
|
||||
return await app_state.midi.start_learn(payload.timeout_seconds, payload.allow_passthrough)
|
||||
|
||||
|
||||
@router.post("/integrations/midi/learn/cancel")
|
||||
async def cancel_midi_learn() -> dict[str, object]:
|
||||
return await app_state.midi.cancel_learn()
|
||||
|
||||
|
||||
@router.post("/integrations/midi/bridge/heartbeat")
|
||||
async def midi_bridge_heartbeat(request: Request, payload: MidiBridgeHeartbeatPayload) -> dict[str, object]:
|
||||
try:
|
||||
await app_state.midi.verify_token(
|
||||
_extract_bearer_token(request),
|
||||
"midi:heartbeat",
|
||||
bridge_id=payload.bridge_id,
|
||||
)
|
||||
return await app_state.midi.receive_heartbeat(
|
||||
payload.model_dump(mode="json", exclude_none=True),
|
||||
request.client.host if request.client else None,
|
||||
)
|
||||
except PermissionError as exc:
|
||||
raise HTTPException(status_code=401, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/integrations/midi/bridge/events")
|
||||
async def midi_bridge_events(request: Request, payload: MidiBridgeEventPayload) -> dict[str, object]:
|
||||
try:
|
||||
await app_state.midi.verify_token(
|
||||
_extract_bearer_token(request),
|
||||
"midi:events",
|
||||
bridge_id=payload.bridge_id,
|
||||
)
|
||||
return await app_state.midi.receive_event(
|
||||
payload.model_dump(mode="json"),
|
||||
request.client.host if request.client else None,
|
||||
)
|
||||
except PermissionError as exc:
|
||||
raise HTTPException(status_code=401, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/triggers/{slug}", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def trigger_event(slug: str, payload: TriggerRequest) -> dict[str, object]:
|
||||
return app_state.triggers.enqueue(slug, payload)
|
||||
|
||||
|
||||
@router.get("/telemetry/live")
|
||||
async def telemetry_live() -> dict[str, object]:
|
||||
return app_state.telemetry.snapshot(len(app_state.triggers.queue))
|
||||
|
||||
|
||||
@router.get("/telemetry/history")
|
||||
async def telemetry_history() -> dict[str, object]:
|
||||
return {"samples": []}
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def events() -> dict[str, object]:
|
||||
return {"items": app_state.telemetry.snapshot()["events"]}
|
||||
|
||||
|
||||
@router.get("/logs")
|
||||
async def logs() -> dict[str, object]:
|
||||
return {"items": app_state.telemetry.snapshot()["events"]}
|
||||
|
||||
|
||||
@router.post("/backups")
|
||||
async def create_backup() -> dict[str, object]:
|
||||
backup = app_state.backups.create_backup()
|
||||
return {
|
||||
"status": "created",
|
||||
"id": backup.id,
|
||||
"label": backup.label,
|
||||
"path": str(backup.archive_path),
|
||||
"manifest": backup.manifest,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/backups")
|
||||
async def list_backups() -> dict[str, object]:
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": backup.id,
|
||||
"label": backup.label,
|
||||
"path": str(backup.archive_path),
|
||||
"created_at": backup.created_at.isoformat(),
|
||||
"manifest": backup.manifest,
|
||||
}
|
||||
for backup in app_state.backups.list_backups()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/backups/{backup_id}/restore")
|
||||
async def restore_backup(backup_id: int) -> dict[str, object]:
|
||||
restored = app_state.backups.restore_backup(str(backup_id))
|
||||
return {
|
||||
"status": "restored",
|
||||
"backup_id": restored.id,
|
||||
"label": restored.label,
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/backups/{backup_id}")
|
||||
async def delete_backup(backup_id: int) -> dict[str, object]:
|
||||
app_state.backups.delete_backup(str(backup_id))
|
||||
return {"deleted": backup_id}
|
||||
Reference in New Issue
Block a user