Private
Public Access
SQLite-Ereignisse und Personenprofile vorbereiten
This commit is contained in:
1 parent
643146599d
commit
f15ab1d399
9 files changed
+637
-21
No files matched your search
+85
-5
@@ -1,25 +1,38 @@
|
||||
"""Lokale WebUI und MJPEG-Livebild; alle Aktoren bleiben simuliert."""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query, Request
|
||||
from fastapi.responses import HTMLResponse, Response, StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .camera import CameraStream
|
||||
from .controller import Controller, MODES
|
||||
from .event_store import EventStore
|
||||
|
||||
|
||||
PAGE = (Path(__file__).parent / "index.html").read_text(encoding="utf-8")
|
||||
PROFILE_CLEANUP_INTERVAL_SECONDS = 3600
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModeInput(BaseModel):
|
||||
mode: str
|
||||
|
||||
|
||||
class ProfileInput(BaseModel):
|
||||
label: str = Field(min_length=1, max_length=80)
|
||||
|
||||
|
||||
class ProfileAssignment(BaseModel):
|
||||
profile_id: int | None
|
||||
|
||||
|
||||
async def require_ui(request: Request):
|
||||
if request.headers.get("X-Halloween-UI") != "1":
|
||||
raise HTTPException(status_code=403, detail="UI-Anfrage erforderlich")
|
||||
@@ -29,16 +42,32 @@ def create_app(demo: bool = False, state_dir: Path | None = None) -> FastAPI:
|
||||
directory = state_dir or Path(os.environ.get("HALLOWEEN_STATE_DIR", "data"))
|
||||
camera = CameraStream(demo=demo)
|
||||
controller = Controller(directory)
|
||||
events = EventStore(directory)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
async def periodic_profile_cleanup():
|
||||
while True:
|
||||
await asyncio.sleep(PROFILE_CLEANUP_INTERVAL_SECONDS)
|
||||
try:
|
||||
events.cleanup_expired()
|
||||
except sqlite3.Error:
|
||||
LOGGER.exception("Abgelaufene Profile konnten nicht gelöscht werden")
|
||||
|
||||
camera.start()
|
||||
yield
|
||||
camera.stop()
|
||||
cleanup_task = asyncio.create_task(periodic_profile_cleanup())
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cleanup_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await cleanup_task
|
||||
camera.stop()
|
||||
|
||||
app = FastAPI(title="Halloween-Grab", lifespan=lifespan)
|
||||
app.state.camera = camera
|
||||
app.state.controller = controller
|
||||
app.state.events = events
|
||||
|
||||
@app.middleware("http")
|
||||
async def security_headers(request: Request, call_next):
|
||||
@@ -57,6 +86,49 @@ def create_app(demo: bool = False, state_dir: Path | None = None) -> FastAPI:
|
||||
async def status():
|
||||
return {"controller": controller.status(), "camera": camera.status()}
|
||||
|
||||
@app.get("/api/events")
|
||||
async def recent_events(limit: int = Query(20, ge=1, le=100)):
|
||||
try:
|
||||
return {"events": events.recent(limit)}
|
||||
except sqlite3.Error as exc:
|
||||
raise HTTPException(status_code=503, detail=f"Datenbank nicht verfügbar: {exc}") from exc
|
||||
|
||||
@app.get("/api/profiles")
|
||||
async def list_profiles():
|
||||
try:
|
||||
return {"profiles": events.profiles()}
|
||||
except sqlite3.Error as exc:
|
||||
raise HTTPException(status_code=503, detail=f"Datenbank nicht verfügbar: {exc}") from exc
|
||||
|
||||
@app.post("/api/profiles", dependencies=[Depends(require_ui)])
|
||||
async def create_profile(payload: ProfileInput):
|
||||
try:
|
||||
return {"profile": events.create_profile(payload.label)}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
except sqlite3.Error as exc:
|
||||
raise HTTPException(status_code=503, detail=f"Profil nicht gespeichert: {exc}") from exc
|
||||
|
||||
@app.patch("/api/events/{event_id}/profile", dependencies=[Depends(require_ui)])
|
||||
async def assign_profile(event_id: int, payload: ProfileAssignment):
|
||||
try:
|
||||
events.assign_profile(event_id, payload.profile_id)
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc.args[0])) from exc
|
||||
except sqlite3.Error as exc:
|
||||
raise HTTPException(status_code=503, detail=f"Zuordnung nicht gespeichert: {exc}") from exc
|
||||
return {"updated": True}
|
||||
|
||||
@app.delete("/api/profiles/{profile_id}", dependencies=[Depends(require_ui)])
|
||||
async def delete_profile(profile_id: int):
|
||||
try:
|
||||
deleted = events.delete_profile(profile_id)
|
||||
except sqlite3.Error as exc:
|
||||
raise HTTPException(status_code=503, detail=f"Profil nicht gelöscht: {exc}") from exc
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="Profil nicht gefunden")
|
||||
return {"deleted": True}
|
||||
|
||||
@app.post("/api/mode", dependencies=[Depends(require_ui)])
|
||||
async def set_mode(payload: ModeInput):
|
||||
if payload.mode not in MODES:
|
||||
@@ -75,6 +147,14 @@ def create_app(demo: bool = False, state_dir: Path | None = None) -> FastAPI:
|
||||
@app.post("/api/test-trigger", dependencies=[Depends(require_ui)])
|
||||
async def test_trigger():
|
||||
accepted, reason = controller.trigger()
|
||||
if accepted:
|
||||
try:
|
||||
events.record_test_trigger(controller.mode)
|
||||
except sqlite3.Error as exc:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail=f"Test gestartet, aber Ereignis nicht gespeichert: {exc}",
|
||||
) from exc
|
||||
return {"accepted": accepted, "reason": reason, "controller": controller.status()}
|
||||
|
||||
@app.post("/api/stop", dependencies=[Depends(require_ui)])
|
||||
|
||||
Reference in new issue
Block a user