# Data Model: Geutebruck Video Surveillance API **Branch**: `001-surveillance-api` | **Date**: 2025-12-08 **Input**: [spec.md](./spec.md) requirements | [research.md](./research.md) technical decisions --- ## Overview This document defines all data entities, their schemas, relationships, validation rules, and state transitions for the Geutebruck Video Surveillance API. **Entity Categories**: - **Authentication**: User, Session, Token - **Surveillance**: Camera, Stream, Recording - **Events**: Event, EventSubscription - **Configuration**: AnalyticsConfig, PTZPreset - **Audit**: AuditLog --- ## 1. Authentication Entities ### 1.1 User Represents an API user with authentication credentials and permissions. **Schema**: ```python class User(BaseModel): id: UUID = Field(default_factory=uuid4) username: str = Field(min_length=3, max_length=50, pattern="^[a-zA-Z0-9_-]+$") email: EmailStr hashed_password: str # bcrypt hash role: UserRole # viewer, operator, administrator permissions: List[Permission] # Granular camera-level permissions is_active: bool = True created_at: datetime updated_at: datetime last_login: Optional[datetime] = None class UserRole(str, Enum): VIEWER = "viewer" # Read-only camera access OPERATOR = "operator" # Camera control + viewing ADMINISTRATOR = "administrator" # Full system configuration class Permission(BaseModel): resource_type: str # "camera", "recording", "analytics" resource_id: int # Channel ID or "*" for all actions: List[str] # ["view", "ptz", "record", "configure"] ``` **Validation Rules**: - `username`: Unique, alphanumeric with dash/underscore only - `email`: Valid email format, unique - `hashed_password`: Never returned in API responses - `role`: Must be one of defined roles - `permissions`: Empty list defaults to role-based permissions **Relationships**: - User → Session (one-to-many): User can have multiple active sessions - User → AuditLog (one-to-many): All user actions logged **Example**: ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "username": "operator1", "email": "operator1@example.com", "role": "operator", "permissions": [ { "resource_type": "camera", "resource_id": 1, "actions": ["view", "ptz"] }, { "resource_type": "camera", "resource_id": 2, "actions": ["view"] } ], "is_active": true, "created_at": "2025-12-01T10:00:00Z", "last_login": "2025-12-08T14:30:00Z" } ``` --- ### 1.2 Session Represents an active authentication session with JWT tokens. **Schema**: ```python class Session(BaseModel): session_id: str = Field(...) # JTI from JWT user_id: UUID access_token_jti: str refresh_token_jti: Optional[str] = None ip_address: str user_agent: str created_at: datetime last_activity: datetime expires_at: datetime class TokenPair(BaseModel): access_token: str # JWT token string refresh_token: str token_type: str = "bearer" expires_in: int # Seconds until access token expires ``` **State Transitions**: ``` Created → Active → Refreshed → Expired/Revoked ``` **Validation Rules**: - `session_id`: Unique, UUID format - `access_token_jti`: Must match JWT jti claim - `ip_address`: Valid IPv4/IPv6 address - `expires_at`: Auto-set based on JWT expiration **Storage**: Redis with TTL matching token expiration **Redis Keys**: ``` session:{user_id}:{session_id} → Session JSON refresh:{user_id}:{refresh_token_jti} → Refresh token metadata ``` --- ## 2. Surveillance Entities ### 2.1 Camera Represents a video input channel/camera with capabilities and status. **Schema**: ```python class Camera(BaseModel): id: int # Channel ID from GeViScope global_id: str # GeViScope GlobalID (UUID) name: str = Field(min_length=1, max_length=100) description: Optional[str] = Field(max_length=500) location: Optional[str] = Field(max_length=200) status: CameraStatus capabilities: CameraCapabilities stream_info: Optional[StreamInfo] = None recording_status: RecordingStatus created_at: datetime updated_at: datetime class CameraStatus(str, Enum): ONLINE = "online" OFFLINE = "offline" ERROR = "error" MAINTENANCE = "maintenance" class CameraCapabilities(BaseModel): has_ptz: bool = False has_video_sensor: bool = False # Motion detection has_contrast_detection: bool = False has_sync_detection: bool = False supported_analytics: List[AnalyticsType] = [] supported_resolutions: List[str] = [] # ["1920x1080", "1280x720"] supported_formats: List[str] = [] # ["h264", "mjpeg"] class AnalyticsType(str, Enum): VMD = "vmd" # Video Motion Detection NPR = "npr" # Number Plate Recognition OBTRACK = "obtrack" # Object Tracking GTECT = "gtect" # Perimeter Protection CPA = "cpa" # Camera Position Analysis ``` **State Transitions**: ``` Offline ⟷ Online ⟷ Error ↓ Maintenance ``` **Validation Rules**: - `id`: Positive integer, corresponds to GeViScope channel ID - `name`: Required, user-friendly camera identifier - `status`: Updated via SDK events - `capabilities`: Populated from GeViScope VideoInputInfo **Relationships**: - Camera → Stream (one-to-many): Multiple concurrent streams per camera - Camera → Recording (one-to-many): Recording segments for this camera - Camera → AnalyticsConfig (one-to-one): Analytics configuration - Camera → PTZPreset (one-to-many): Saved PTZ positions **Example**: ```json { "id": 5, "global_id": "a7b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6", "name": "Entrance Camera", "description": "Main entrance monitoring", "location": "Building A - Main Entrance", "status": "online", "capabilities": { "has_ptz": true, "has_video_sensor": true, "has_contrast_detection": true, "has_sync_detection": true, "supported_analytics": ["vmd", "obtrack"], "supported_resolutions": ["1920x1080", "1280x720"], "supported_formats": ["h264", "mjpeg"] }, "recording_status": { "is_recording": true, "mode": "continuous", "start_time": "2025-12-08T00:00:00Z" } } ``` --- ### 2.2 Stream Represents an active video stream session. **Schema**: ```python class Stream(BaseModel): stream_id: UUID = Field(default_factory=uuid4) camera_id: int # Channel ID user_id: UUID stream_url: HttpUrl # Authenticated URL to GeViScope stream format: str # "h264", "mjpeg" resolution: str # "1920x1080" fps: int = Field(ge=1, le=60) quality: int = Field(ge=1, le=100) # Quality percentage started_at: datetime last_activity: datetime expires_at: datetime status: StreamStatus class StreamStatus(str, Enum): INITIALIZING = "initializing" ACTIVE = "active" PAUSED = "paused" STOPPED = "stopped" ERROR = "error" class StreamRequest(BaseModel): """Request model for initiating a stream""" format: str = "h264" resolution: Optional[str] = None # Default: camera's max resolution fps: Optional[int] = None # Default: camera's max FPS quality: int = Field(default=90, ge=1, le=100) class StreamResponse(BaseModel): """Response containing stream access details""" stream_id: UUID camera_id: int stream_url: HttpUrl # Token-authenticated URL format: str resolution: str fps: int expires_at: datetime # Stream URL expiration websocket_url: Optional[HttpUrl] = None # For WebSocket-based streams ``` **State Transitions**: ``` Initializing → Active ⟷ Paused → Stopped ↓ Error ``` **Validation Rules**: - `camera_id`: Must reference existing, online camera - `stream_url`: Contains time-limited JWT token - `expires_at`: Default 1 hour from creation - `format`, `resolution`, `fps`: Must be supported by camera **Lifecycle**: 1. Client requests stream: `POST /cameras/{id}/stream` 2. API generates token-authenticated URL 3. Client connects directly to GeViScope stream URL 4. Stream auto-expires after TTL 5. Client can extend via API: `POST /streams/{id}/extend` --- ### 2.3 Recording Represents a video recording segment. **Schema**: ```python class Recording(BaseModel): id: UUID = Field(default_factory=uuid4) camera_id: int start_time: datetime end_time: Optional[datetime] = None # None if still recording duration_seconds: Optional[int] = None file_size_bytes: Optional[int] = None trigger: RecordingTrigger status: RecordingStatus export_url: Optional[HttpUrl] = None # If exported metadata: RecordingMetadata created_at: datetime class RecordingTrigger(str, Enum): SCHEDULED = "scheduled" # Time-based schedule EVENT = "event" # Triggered by alarm/analytics MANUAL = "manual" # User-initiated CONTINUOUS = "continuous" # Always recording class RecordingStatus(str, Enum): RECORDING = "recording" COMPLETED = "completed" FAILED = "failed" EXPORTING = "exporting" EXPORTED = "exported" class RecordingMetadata(BaseModel): event_id: Optional[str] = None # If event-triggered pre_alarm_seconds: int = 0 post_alarm_seconds: int = 0 tags: List[str] = [] notes: Optional[str] = None class RecordingQueryParams(BaseModel): camera_id: Optional[int] = None start_time: Optional[datetime] = None end_time: Optional[datetime] = None trigger: Optional[RecordingTrigger] = None limit: int = Field(default=50, ge=1, le=1000) offset: int = Field(default=0, ge=0) ``` **State Transitions**: ``` Recording → Completed ↓ Exporting → Exported ↓ Failed ``` **Validation Rules**: - `camera_id`: Must exist - `start_time`: Cannot be in future - `end_time`: Must be after start_time - `file_size_bytes`: Calculated from ring buffer **Relationships**: - Recording → Camera (many-to-one) - Recording → Event (many-to-one, optional) **Ring Buffer Handling**: - Oldest recordings automatically deleted when buffer full - `retention_policy` determines minimum retention period - API exposes capacity warnings --- ## 3. Event Entities ### 3.1 Event Represents a surveillance event (alarm, analytics, system). **Schema**: ```python class Event(BaseModel): id: UUID = Field(default_factory=uuid4) event_type: EventType camera_id: Optional[int] = None # None for system events timestamp: datetime severity: EventSeverity data: EventData # Type-specific event data foreign_key: Optional[str] = None # External system correlation acknowledged: bool = False acknowledged_by: Optional[UUID] = None acknowledged_at: Optional[datetime] = None class EventType(str, Enum): # Analytics events MOTION_DETECTED = "motion_detected" OBJECT_TRACKED = "object_tracked" LICENSE_PLATE = "license_plate" PERIMETER_BREACH = "perimeter_breach" CAMERA_TAMPER = "camera_tamper" # System events CAMERA_ONLINE = "camera_online" CAMERA_OFFLINE = "camera_offline" RECORDING_STARTED = "recording_started" RECORDING_STOPPED = "recording_stopped" STORAGE_WARNING = "storage_warning" # Alarm events ALARM_TRIGGERED = "alarm_triggered" ALARM_CLEARED = "alarm_cleared" class EventSeverity(str, Enum): INFO = "info" WARNING = "warning" ERROR = "error" CRITICAL = "critical" class EventData(BaseModel): """Base class for type-specific event data""" pass class MotionDetectedData(EventData): zone: str confidence: float = Field(ge=0.0, le=1.0) snapshot_url: Optional[HttpUrl] = None class LicensePlateData(EventData): plate_number: str country_code: str confidence: float = Field(ge=0.0, le=1.0) snapshot_url: Optional[HttpUrl] = None is_watchlist_match: bool = False class ObjectTrackedData(EventData): tracking_id: str object_type: str # "person", "vehicle" zone_entered: Optional[str] = None zone_exited: Optional[str] = None dwell_time_seconds: Optional[int] = None ``` **Validation Rules**: - `event_type`: Must be valid EventType - `camera_id`: Required for camera events, None for system events - `timestamp`: Auto-set to current time if not provided - `severity`: Must match event type severity mapping **Relationships**: - Event → Camera (many-to-one, optional) - Event → Recording (one-to-one, optional) - Event → User (acknowledged_by, many-to-one, optional) **Example**: ```json { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "event_type": "motion_detected", "camera_id": 5, "timestamp": "2025-12-08T14:45:23Z", "severity": "warning", "data": { "zone": "entrance", "confidence": 0.95, "snapshot_url": "https://api.example.com/snapshots/abc123.jpg" }, "acknowledged": false } ``` --- ### 3.2 EventSubscription Represents a WebSocket client's event subscription. **Schema**: ```python class EventSubscription(BaseModel): subscription_id: UUID = Field(default_factory=uuid4) user_id: UUID connection_id: str # WebSocket connection identifier filters: EventFilter created_at: datetime last_heartbeat: datetime class EventFilter(BaseModel): event_types: Optional[List[EventType]] = None # None = all types camera_ids: Optional[List[int]] = None # None = all cameras severity: Optional[EventSeverity] = None # Minimum severity include_acknowledged: bool = False class EventNotification(BaseModel): """WebSocket message format""" subscription_id: UUID event: Event sequence_number: int # For detecting missed events ``` **Validation Rules**: - `filters.camera_ids`: User must have view permission for each camera - `last_heartbeat`: Updated every 30 seconds, timeout after 90 seconds **Storage**: Redis with 5-minute retention for reconnection --- ## 4. Configuration Entities ### 4.1 AnalyticsConfig Configuration for video analytics on a camera. **Schema**: ```python class AnalyticsConfig(BaseModel): camera_id: int analytics_type: AnalyticsType enabled: bool = False config: AnalyticsTypeConfig # Type-specific configuration updated_at: datetime updated_by: UUID class VMDConfig(AnalyticsTypeConfig): """Video Motion Detection configuration""" zones: List[DetectionZone] sensitivity: int = Field(ge=1, le=10, default=5) min_object_size: int = Field(ge=1, le=100, default=10) # Percentage ignore_zones: List[DetectionZone] = [] class DetectionZone(BaseModel): name: str polygon: List[Point] # List of x,y coordinates class Point(BaseModel): x: int = Field(ge=0, le=100) # Percentage of frame width y: int = Field(ge=0, le=100) # Percentage of frame height class NPRConfig(AnalyticsTypeConfig): """Number Plate Recognition configuration""" zones: List[DetectionZone] country_codes: List[str] = ["*"] # ["US", "DE"] or "*" for all min_confidence: float = Field(ge=0.0, le=1.0, default=0.7) watchlist: List[str] = [] # List of plate numbers to alert on class OBTRACKConfig(AnalyticsTypeConfig): """Object Tracking configuration""" object_types: List[str] = ["person", "vehicle"] min_dwell_time: int = Field(ge=0, default=5) # Seconds count_lines: List[CountLine] = [] class CountLine(BaseModel): name: str point_a: Point point_b: Point direction: str # "in", "out", "both" ``` **Validation Rules**: - `camera_id`: Must exist and support analytics_type - `zones`: At least one zone required when enabled=True - `polygon`: Minimum 3 points, closed polygon - `sensitivity`: Higher = more sensitive **Example**: ```json { "camera_id": 5, "analytics_type": "vmd", "enabled": true, "config": { "zones": [ { "name": "entrance", "polygon": [ {"x": 10, "y": 10}, {"x": 90, "y": 10}, {"x": 90, "y": 90}, {"x": 10, "y": 90} ] } ], "sensitivity": 7, "min_object_size": 5 }, "updated_at": "2025-12-08T10:00:00Z" } ``` --- ### 4.2 PTZPreset Saved PTZ camera position. **Schema**: ```python class PTZPreset(BaseModel): id: int # Preset ID (1-255) camera_id: int name: str = Field(min_length=1, max_length=50) pan: int = Field(ge=-180, le=180) # Degrees tilt: int = Field(ge=-90, le=90) # Degrees zoom: int = Field(ge=0, le=100) # Percentage created_at: datetime created_by: UUID updated_at: datetime class PTZCommand(BaseModel): """PTZ control command""" action: PTZAction speed: Optional[int] = Field(default=50, ge=1, le=100) preset_id: Optional[int] = None # For goto_preset action class PTZAction(str, Enum): PAN_LEFT = "pan_left" PAN_RIGHT = "pan_right" TILT_UP = "tilt_up" TILT_DOWN = "tilt_down" ZOOM_IN = "zoom_in" ZOOM_OUT = "zoom_out" STOP = "stop" GOTO_PRESET = "goto_preset" SAVE_PRESET = "save_preset" ``` **Validation Rules**: - `id`: Unique per camera, 1-255 range - `camera_id`: Must exist and have PTZ capability - `name`: Unique per camera --- ## 5. Audit Entities ### 5.1 AuditLog Audit trail for all privileged operations. **Schema**: ```python class AuditLog(BaseModel): id: UUID = Field(default_factory=uuid4) timestamp: datetime user_id: UUID username: str action: str # "camera.ptz", "recording.start", "user.create" resource_type: str # "camera", "recording", "user" resource_id: str outcome: AuditOutcome ip_address: str user_agent: str details: Optional[dict] = None # Action-specific metadata class AuditOutcome(str, Enum): SUCCESS = "success" FAILURE = "failure" PARTIAL = "partial" ``` **Validation Rules**: - `timestamp`: Auto-set, immutable - `user_id`: Must exist - `action`: Format "{resource}.{operation}" **Storage**: Append-only, never deleted, indexed by user_id and timestamp **Example**: ```json { "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "timestamp": "2025-12-08T14:50:00Z", "user_id": "550e8400-e29b-41d4-a716-446655440000", "username": "operator1", "action": "camera.ptz", "resource_type": "camera", "resource_id": "5", "outcome": "success", "ip_address": "192.168.1.100", "user_agent": "Mozilla/5.0...", "details": { "ptz_action": "pan_left", "speed": 50 } } ``` --- ## Entity Relationships Diagram ``` ┌─────────┐ ┌─────────────┐ ┌────────────┐ │ User │──1:N──│ Session │ │ AuditLog │ └────┬────┘ └─────────────┘ └─────┬──────┘ │ │ │1:N │N:1 │ │ ▼ │ ┌─────────────────┐ │ │ EventSubscription│ │ └─────────────────┘ │ │ ┌────────────┐ ┌─────────┐ ┌──────▼─────┐ │ Camera │──1:N──│ Stream │ │ Recording │ └─────┬──────┘ └─────────┘ └──────┬─────┘ │ │ │1:N │N:1 ▼ │ ┌─────────────────┐ │ │ AnalyticsConfig │ │ └─────────────────┘ │ │ │ │1:N │ ▼ ▼ ┌─────────────┐ ┌────────────────────────────┐ │ PTZPreset │ │ Event │ └─────────────┘ └────────────────────────────┘ ``` --- ## Validation Rules Summary | Entity | Key Validations | |--------|-----------------| | User | Unique username/email, valid role, bcrypt password | | Session | Valid JWT, IP address, TTL enforced | | Camera | Valid channel ID, status from SDK, capabilities match | | Stream | Camera online, token authentication, supported formats | | Recording | Valid time range, camera exists, ring buffer aware | | Event | Valid type, severity, camera permissions | | EventSubscription | User has camera permissions | | AnalyticsConfig | Camera supports type, valid zones/settings | | PTZPreset | Camera has PTZ, valid coordinates | | AuditLog | Immutable, complete metadata | --- ## State Machine Definitions ### Camera Status State Machine ``` [Offline] ──detect_online──▶ [Online] ──detect_offline──▶ [Offline] │ ▲ │ │ └───recover_error────────┘ │ └──detect_error──▶ [Error] │ ┌─────────────────────┘ ▼ [Maintenance] ──restore──▶ [Online] ``` ### Recording Status State Machine ``` [Recording] ──complete──▶ [Completed] ──export──▶ [Exporting] ──finish──▶ [Exported] │ │ └──error──▶ [Failed] ◀──────────────────────────┘ ``` ### Stream Status State Machine ``` [Initializing] ──ready──▶ [Active] ──pause──▶ [Paused] ──resume──▶ [Active] │ │ └──stop──▶ [Stopped] ◀──────────────────┘ │ └──error──▶ [Error] ``` --- **Phase 1 Status**: ✅ Data model complete **Next**: Generate OpenAPI contracts