Files
geutebruck-api/specs/001-surveillance-api/data-model.md
Geutebruck API Developer dd2278b39a Complete Phase 0 and Phase 1 design documentation
- Add comprehensive research.md with SDK integration decisions
- Add complete data-model.md with 7 entities and relationships
- Add OpenAPI 3.0 specification (contracts/openapi.yaml)
- Add developer quickstart.md guide
- Add comprehensive tasks.md with 215 tasks organized by user story
- Update plan.md with complete technical context
- Add SDK_INTEGRATION_LESSONS.md capturing critical knowledge
- Add .gitignore for Python and C# projects
- Include GeViScopeConfigReader and GeViSoftConfigReader tools

Phase 1 Design Complete:
 Architecture: Python FastAPI + C# gRPC Bridge + GeViScope SDK
 10 user stories mapped to tasks (MVP = US1-4)
 Complete API contract with 17 endpoints
 Data model with User, Camera, Stream, Event, Recording, Analytics
 TDD approach enforced with 80+ test tasks

Ready for Phase 2: Implementation

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 07:39:55 +01:00

22 KiB

Data Model: Geutebruck Video Surveillance API

Branch: 001-surveillance-api | Date: 2025-12-08 Input: spec.md requirements | 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:

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:

{
  "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:

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:

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:

{
  "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:

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:

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:

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:

{
  "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:

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:

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:

{
  "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:

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:

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:

{
  "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