feat: add fire prevention management platform
This commit is contained in:
97
backend/events.py
Normal file
97
backend/events.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import uuid
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
EVENT_STATUSES = {"pending", "acknowledged", "resolved"}
|
||||
|
||||
|
||||
class EventStore:
|
||||
def __init__(self, max_events: int = 500) -> None:
|
||||
self._events: deque[dict[str, Any]] = deque(maxlen=max_events)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def create(
|
||||
self,
|
||||
session_id: str,
|
||||
classes: list[str],
|
||||
detections: list[dict[str, Any]],
|
||||
notification_channels: dict[str, bool],
|
||||
) -> dict[str, Any]:
|
||||
now = datetime.now().astimezone()
|
||||
confidences = [
|
||||
float(detection.get("confidence", 0.0))
|
||||
for detection in detections
|
||||
if detection.get("class") in classes
|
||||
]
|
||||
event = {
|
||||
"id": uuid.uuid4().hex[:12],
|
||||
"created_at": now.isoformat(timespec="seconds"),
|
||||
"session_id": session_id,
|
||||
"classes": classes,
|
||||
"max_confidence": round(max(confidences, default=0.0), 6),
|
||||
"detection_count": len(detections),
|
||||
"status": "pending",
|
||||
"notification_channels": notification_channels,
|
||||
"handled_at": None,
|
||||
}
|
||||
with self._lock:
|
||||
self._events.appendleft(event)
|
||||
return dict(event)
|
||||
|
||||
def list(
|
||||
self,
|
||||
limit: int = 100,
|
||||
status: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
with self._lock:
|
||||
events = [dict(event) for event in self._events]
|
||||
if status:
|
||||
events = [event for event in events if event["status"] == status]
|
||||
return events[: max(1, min(limit, 500))]
|
||||
|
||||
def update(self, event_id: str, status: str) -> dict[str, Any] | None:
|
||||
if status not in EVENT_STATUSES:
|
||||
raise ValueError(f"Unsupported event status: {status}")
|
||||
with self._lock:
|
||||
for event in self._events:
|
||||
if event["id"] != event_id:
|
||||
continue
|
||||
event["status"] = status
|
||||
event["handled_at"] = (
|
||||
None
|
||||
if status == "pending"
|
||||
else datetime.now().astimezone().isoformat(
|
||||
timespec="seconds"
|
||||
)
|
||||
)
|
||||
return dict(event)
|
||||
return None
|
||||
|
||||
def summary(self) -> dict[str, int]:
|
||||
today = datetime.now().astimezone().date()
|
||||
with self._lock:
|
||||
events = [dict(event) for event in self._events]
|
||||
today_events = [
|
||||
event
|
||||
for event in events
|
||||
if datetime.fromisoformat(event["created_at"]).date() == today
|
||||
]
|
||||
return {
|
||||
"total": len(events),
|
||||
"today": len(today_events),
|
||||
"pending": sum(
|
||||
event["status"] == "pending" for event in events
|
||||
),
|
||||
"acknowledged": sum(
|
||||
event["status"] == "acknowledged" for event in events
|
||||
),
|
||||
"resolved": sum(
|
||||
event["status"] == "resolved" for event in events
|
||||
),
|
||||
"fire": sum("fire" in event["classes"] for event in events),
|
||||
"smoke": sum("smoke" in event["classes"] for event in events),
|
||||
}
|
||||
@@ -12,9 +12,11 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import BaseModel
|
||||
from ultralytics import YOLO
|
||||
|
||||
from .alerting import AlertManager
|
||||
from .events import EVENT_STATUSES, EventStore
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
FRONTEND_DIR = PROJECT_ROOT / "frontend"
|
||||
@@ -32,6 +34,11 @@ ALERT_MANAGER = AlertManager(
|
||||
confirm_frames=int(os.getenv("ALERT_CONFIRM_FRAMES", "3")),
|
||||
cooldown_seconds=float(os.getenv("ALERT_COOLDOWN_SECONDS", "60")),
|
||||
)
|
||||
EVENT_STORE = EventStore()
|
||||
|
||||
|
||||
class EventStatusUpdate(BaseModel):
|
||||
status: str
|
||||
|
||||
app = FastAPI(title="Smoke Fire Detector API", version="0.1.0")
|
||||
app.add_middleware(
|
||||
@@ -132,6 +139,16 @@ async def detect(
|
||||
"notification_channels": ALERT_MANAGER.channels,
|
||||
}
|
||||
)
|
||||
if result["alert"]["triggered"]:
|
||||
result["event"] = EVENT_STORE.create(
|
||||
session_id=session_id or "single-image",
|
||||
classes=result["alert"]["classes"],
|
||||
detections=result["detections"],
|
||||
notification_channels=result["alert"].get(
|
||||
"notification_channels",
|
||||
ALERT_MANAGER.channels,
|
||||
),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -141,6 +158,49 @@ def reset_detection_session(session_id: str) -> dict[str, str]:
|
||||
return {"status": "reset"}
|
||||
|
||||
|
||||
@app.get("/api/events")
|
||||
def list_events(
|
||||
limit: int = 100,
|
||||
status: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if status and status not in EVENT_STATUSES:
|
||||
raise HTTPException(status_code=400, detail="Invalid event status")
|
||||
return {
|
||||
"events": EVENT_STORE.list(limit=limit, status=status),
|
||||
"summary": EVENT_STORE.summary(),
|
||||
}
|
||||
|
||||
|
||||
@app.patch("/api/events/{event_id}")
|
||||
def update_event(
|
||||
event_id: str,
|
||||
update: EventStatusUpdate,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
event = EVENT_STORE.update(event_id, update.status)
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=400, detail=str(error)) from error
|
||||
if event is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
return event
|
||||
|
||||
|
||||
@app.get("/api/dashboard")
|
||||
def dashboard() -> dict[str, Any]:
|
||||
return {
|
||||
"summary": EVENT_STORE.summary(),
|
||||
"recent_events": EVENT_STORE.list(limit=6),
|
||||
"system": health(),
|
||||
"detection": {
|
||||
"confidence": CONFIDENCE,
|
||||
"iou": IOU,
|
||||
"image_size": IMAGE_SIZE,
|
||||
"confirm_frames": ALERT_MANAGER.confirm_frames,
|
||||
"cooldown_seconds": ALERT_MANAGER.cooldown_seconds,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def frontend() -> FileResponse:
|
||||
return FileResponse(FRONTEND_DIR / "index.html")
|
||||
|
||||
Reference in New Issue
Block a user