Files
geutebruck-api/specs/001-surveillance-api/data-model.md
Geutebruck API Developer d2c6937665 docs: Update specifications to reflect configuration management implementation
Updated all spec-kit documents to document the implemented configuration
management features (User Story 12):

Changes:
- spec.md: Added User Story 12 with implementation status and functional
  requirements (FR-039 through FR-045)
- plan.md: Added Phase 2 (Configuration Management) as completed, updated
  phase status and last updated date
- data-model.md: Added GCoreServer entity with schema, validation rules,
  CRUD status, and critical implementation details
- tasks.md: Added Phase 13 for User Story 12 with implementation summary,
  updated task counts and dependencies
- tasks-revised-mvp.md: Added configuration management completion notice

Implementation Highlights:
- G-Core Server CRUD (CREATE, READ, DELETE working; UPDATE has known bug)
- Action Mapping CRUD (all operations working)
- SetupClient integration for .set file operations
- Critical cascade deletion bug fix (delete in reverse order)
- Comprehensive test scripts and verification tools

Documentation: SERVER_CRUD_IMPLEMENTATION.md, CRITICAL_BUG_FIX_DELETE.md

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 20:57:16 +01:00

36 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
  }
}

6. Unified Architecture Entities

6.1 GeViScope Instance

Represents a configured GeViScope server (GSCServer) connection.

Schema:

class GeViScopeInstance(BaseModel):
    id: str = Field(..., pattern="^[a-z][a-z0-9-]*$")  # "main", "parking", "warehouse"
    name: str = Field(min_length=1, max_length=100)
    description: Optional[str] = Field(max_length=500)
    host: str  # Hostname or IP address
    port: int = Field(default=7700, ge=1, le=65535)
    username: str
    # Password stored securely (not in database)
    is_default: bool = False
    enabled: bool = True
    connection_status: ConnectionStatus
    last_connected: Optional[datetime] = None
    camera_count: int = 0
    monitor_count: int = 0
    sdk_version: Optional[str] = None
    created_at: datetime
    updated_at: datetime

class ConnectionStatus(str, Enum):
    CONNECTED = "connected"
    DISCONNECTED = "disconnected"
    CONNECTING = "connecting"
    ERROR = "error"

Validation Rules:

  • id: Lowercase alphanumeric with dashes, starts with letter
  • host: Valid hostname or IP address
  • is_default: Only one instance can be default
  • enabled: Disabled instances not available for operations

Relationships:

  • GeViScopeInstance → Camera (one-to-many)
  • GeViScopeInstance → Monitor (one-to-many)
  • GeViScopeInstance → CrossSwitchRoute (one-to-many)

Example:

{
  "id": "main",
  "name": "Main Building",
  "description": "Primary surveillance system",
  "host": "localhost",
  "port": 7700,
  "username": "sysadmin",
  "is_default": true,
  "enabled": true,
  "connection_status": "connected",
  "last_connected": "2025-12-10T14:00:00Z",
  "camera_count": 13,
  "monitor_count": 256,
  "sdk_version": "7.9.975.68"
}

6.2 Monitor (Video Output)

Represents a video output channel (logical display channel, not physical display).

Schema:

class Monitor(BaseModel):
    id: int  # Monitor ID from GeViScope (1-256)
    geviscope_instance_id: str  # Which GeViScope instance
    global_id: str  # GeViScope GlobalID
    name: str = Field(min_length=1, max_length=100)
    description: Optional[str] = Field(max_length=500)
    is_active: bool = True
    is_enabled: bool = True
    current_camera_id: Optional[int] = None  # Currently routed camera
    current_route_id: Optional[UUID] = None
    status: MonitorStatus
    created_at: datetime
    updated_at: datetime

class MonitorStatus(str, Enum):
    ONLINE = "online"
    OFFLINE = "offline"
    ERROR = "error"

Important Notes:

  • Monitors are logical routing channels, not physical displays
  • CrossSwitch routes video to monitors at the server level
  • Viewer applications (GSCView) required to actually display monitor video
  • Monitor enumeration may return more IDs than physical outputs exist

Validation Rules:

  • id: Positive integer, unique within GeViScope instance
  • geviscope_instance_id: Must reference existing instance
  • current_camera_id: If set, must reference existing camera

Relationships:

  • Monitor → GeViScopeInstance (many-to-one)
  • Monitor → Camera (many-to-one, optional via current_camera_id)
  • Monitor → CrossSwitchRoute (one-to-many)

Example:

{
  "id": 1,
  "geviscope_instance_id": "main",
  "global_id": "b8c2d4e6-f7a8-49b0-c1d2-e3f4a5b6c7d8",
  "name": "Video Output 1",
  "description": "Main control room display",
  "is_active": true,
  "is_enabled": true,
  "current_camera_id": 101038,
  "current_route_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "online"
}

6.3 CrossSwitch Route

Represents an active or historical video routing (camera → monitor).

Schema:

class CrossSwitchRoute(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    geviscope_instance_id: str  # Which GeViScope instance
    camera_id: int  # Video input channel
    monitor_id: int  # Video output channel
    mode: int = 0  # 0=normal (sm_Normal)
    is_active: bool = True  # False when cleared
    executed_at: datetime
    executed_by: UUID  # User who created route
    executed_by_username: str
    cleared_at: Optional[datetime] = None
    cleared_by: Optional[UUID] = None
    sdk_success: bool = True  # SDK execution result
    sdk_message: Optional[str] = None
    camera_name: Optional[str] = None  # Cached camera info
    monitor_name: Optional[str] = None  # Cached monitor info

class CrossSwitchRequest(BaseModel):
    """Request model for cross-switch operation"""
    camera_id: int = Field(ge=1)
    monitor_id: int = Field(ge=1)
    mode: int = Field(default=0, ge=0, le=2)

class CrossSwitchResponse(BaseModel):
    """Response model for cross-switch operation"""
    success: bool
    message: str
    route: CrossSwitchRoute

State Transitions:

Created (is_active=True) → Cleared (is_active=False)

Validation Rules:

  • camera_id: Must exist and be online in the GeViScope instance
  • monitor_id: Must exist in the GeViScope instance
  • mode: 0=normal (sm_Normal), other modes reserved
  • executed_by: Must be authenticated user

Relationships:

  • CrossSwitchRoute → GeViScopeInstance (many-to-one)
  • CrossSwitchRoute → Camera (many-to-one)
  • CrossSwitchRoute → Monitor (many-to-one)
  • CrossSwitchRoute → User (executed_by, many-to-one)

Example:

{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "geviscope_instance_id": "main",
  "camera_id": 101038,
  "monitor_id": 1,
  "mode": 0,
  "is_active": true,
  "executed_at": "2025-12-10T14:30:00Z",
  "executed_by": "550e8400-e29b-41d4-a716-446655440000",
  "executed_by_username": "admin",
  "sdk_success": true,
  "sdk_message": "Cross-switch executed successfully",
  "camera_name": "Entrance Camera",
  "monitor_name": "Video Output 1"
}

6.4 Alarm (GeViSoft)

Represents a system-wide alarm configuration in GeViSoft.

Schema:

class Alarm(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    alarm_id: int  # GeViSoft alarm ID
    name: str = Field(min_length=1, max_length=100)
    description: Optional[str] = Field(max_length=500)
    priority: AlarmPriority
    monitor_group_id: Optional[int] = None
    cameras: List[int] = []  # Camera channels
    start_actions: List[str] = []  # Actions on alarm start
    stop_actions: List[str] = []  # Actions on alarm stop
    acknowledge_actions: List[str] = []  # Actions on acknowledge
    is_active: bool = False
    triggered_at: Optional[datetime] = None
    acknowledged_at: Optional[datetime] = None
    acknowledged_by: Optional[UUID] = None
    created_at: datetime
    updated_at: datetime

class AlarmPriority(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"

Validation Rules:

  • alarm_id: Positive integer, unique
  • cameras: All camera IDs must exist
  • start_actions: Valid GeViSoft action strings
  • is_active: Set true when triggered, false when acknowledged/stopped

Relationships:

  • Alarm → Camera (many-to-many via cameras list)
  • Alarm → User (acknowledged_by, many-to-one, optional)

6.5 Action Mapping

Represents automation rules in GeViSoft (input action → output actions).

Schema:

class ActionMapping(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    name: str = Field(min_length=1, max_length=100)
    description: Optional[str] = Field(max_length=500)
    input_action: str  # GeViSoft action that triggers mapping
    output_actions: List[str]  # Actions to execute
    geviscope_instance_scope: Optional[str] = None  # Limit to specific instance
    enabled: bool = True
    execution_count: int = 0
    last_executed: Optional[datetime] = None
    created_at: datetime
    updated_at: datetime
    created_by: UUID

class ActionMappingExecution(BaseModel):
    """Execution log for action mappings"""
    id: UUID = Field(default_factory=uuid4)
    mapping_id: UUID
    input_action: str
    output_actions_executed: List[str]
    success: bool
    error_message: Optional[str] = None
    executed_at: datetime

Validation Rules:

  • input_action: Valid GeViSoft action format
  • output_actions: Valid GeViSoft action formats
  • geviscope_instance_scope: If set, must reference existing instance

Example:

{
  "id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
  "name": "Motion Detection Alert",
  "description": "Route cameras to monitors when motion detected",
  "input_action": "VMD_Start(101038)",
  "output_actions": [
    "CrossSwitch(101038, 1, 0)",
    "SendMail(security@example.com, Motion Detected)"
  ],
  "geviscope_instance_scope": "main",
  "enabled": true,
  "execution_count": 42,
  "last_executed": "2025-12-10T14:00:00Z"
}

6.6 G-Core Server

Represents a configured G-Core server (remote surveillance server connection) in GeViSoft.

Schema:

class GCoreServer(BaseModel):
    id: str = Field(..., pattern="^[0-9]+$")  # Numeric string ID
    alias: str = Field(min_length=1, max_length=100)  # Display name
    host: str = Field(min_length=1, max_length=255)  # IP address or hostname
    user: str = Field(default="admin", max_length=100)  # Username
    password: str = Field(max_length=100)  # Password (encrypted in storage)
    enabled: bool = True  # Enable/disable server
    deactivate_echo: bool = False  # Deactivate echo
    deactivate_live_check: bool = False  # Deactivate live check
    created_at: datetime
    updated_at: datetime
    created_by: UUID

class GCoreServerInput(BaseModel):
    """Request model for creating/updating G-Core server"""
    alias: str = Field(min_length=1, max_length=100)
    host: str = Field(min_length=1, max_length=255)
    user: str = Field(default="admin", max_length=100)
    password: str = Field(max_length=100)
    enabled: bool = True
    deactivate_echo: bool = False
    deactivate_live_check: bool = False

Implementation Notes:

  • ID is auto-incremented based on highest existing numeric server ID
  • Field order in configuration tree must be: Alias, DeactivateEcho, DeactivateLiveCheck, Enabled, Host, Password, User
  • Bool fields must use type code 1 (bool) not type code 4 (int32) for GeViSet compatibility
  • Password stored as plain text in GeViSoft configuration (SDK limitation)

Validation Rules:

  • id: Numeric string, auto-generated on CREATE
  • alias: Required, display name for server
  • host: Required, valid IP address or hostname
  • user: Defaults to "admin" if not provided
  • enabled: Controls whether server is active

Relationships:

  • GCoreServer → User (created_by, many-to-one)
  • Configuration stored in GeViSoft .set file under GeViGCoreServer folder

Example:

{
  "id": "2",
  "alias": "Remote Office Server",
  "host": "192.168.1.100",
  "user": "admin",
  "password": "secure_password",
  "enabled": true,
  "deactivate_echo": false,
  "deactivate_live_check": false,
  "created_at": "2025-12-16T10:00:00Z"
}

CRUD Operations (2025-12-16):

  • CREATE: Working - creates server with auto-incremented ID
  • READ: Working - reads all servers or single server by ID
  • ⚠️ UPDATE: Known bug - requires fix for "Server ID is required" error
  • DELETE: Working - deletes server by ID

Critical Implementation Details:

  • Cascade Deletion Prevention: When deleting multiple servers, always delete in reverse order (highest ID first) to prevent ID shifting
  • Bool Type Handling: Must write as bool type (type code 1) not int32, even though GeViSoft stores as int32
  • SetupClient Required: All configuration changes must use SetupClient for download/upload to ensure atomicity

Entity Relationships Diagram

┌────────────────────┐
│ GeViScopeInstance  │──┐
└────────────────────┘  │
                        │1:N
           ┌────────────┴──────────────┐
           │                           │
           ▼                           ▼
┌────────────┐       ┌────────────┐   ┌─────────────────┐
│  Camera    │──1:N──│ Stream  │       │    Monitor      │
└─────┬──────┘       └─────────┘       └────────┬────────┘
      │                                          │
      │1:N                                       │1:N
      ▼                                          ▼
┌─────────────────┐                   ┌───────────────────┐
│ AnalyticsConfig │                   │ CrossSwitchRoute  │
└─────────────────┘                   └─────────┬─────────┘
      │                                          │
      │1:N                                       │N:1
      ▼                                          │
┌─────────────┐                                 │
│ PTZPreset   │                                 │
└─────────────┘                                 ▼
                                      ┌─────────┐
┌─────────┐       ┌─────────────┐    │  User   │──1:N──┌──────────────────┐
│  User   │──1:N──│   Session   │    └────┬────┘       │ EventSubscription│
└────┬────┘       └─────────────┘         │            └──────────────────┘
     │                                     │1:N
     │1:N                                  │
     ▼                                     ▼
┌────────────┐                      ┌──────────────┐
│  AuditLog  │                      │    Alarm     │
└────────────┘                      └──────────────┘
                                          │M:N
                                          ▼
                                    ┌────────────┐
                                    │  Camera    │
                                    └────────────┘

┌──────────────────┐       ┌──────────┐
│  ActionMapping   │──1:N──│   Event  │
└──────────────────┘       └────┬─────┘
                                │N:1
                                ▼
                          ┌──────────────┐
                          │  Recording   │
                          └──────────────┘

Validation Rules Summary

Entity Key Validations
User Unique username/email, valid role, bcrypt password
Session Valid JWT, IP address, TTL enforced
GeViScopeInstance Unique ID, valid host, only one default
Camera Valid channel ID, status from SDK, capabilities match
Monitor Valid ID within instance, scoped by instance ID
CrossSwitchRoute Camera/monitor exist and online, valid user
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
Alarm (GeViSoft) Valid alarm ID, cameras exist, valid actions
ActionMapping Valid action formats, instance scope if specified
GCoreServer Numeric ID, valid host, bool type handling
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]

CrossSwitch Route State Machine

[Created] (is_active=True) ──clear_monitor──▶ [Cleared] (is_active=False)
     │
     └──new_crossswitch──▶ [Replaced] (old route cleared, new route created)

Monitor Assignment State Machine

[Empty] (no camera) ──crossswitch──▶ [Assigned] (camera routed)
                                          │
                                          └──clear──▶ [Empty]
                                          │
                                          └──crossswitch──▶ [Reassigned] (new camera)

Phase 1 Status: Data model updated with unified architecture and configuration management Last Updated: 2025-12-16 Next: Implement multi-instance SDK bridge connections