# 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 } } ``` --- ## 6. Unified Architecture Entities ### 6.1 GeViScope Instance Represents a configured GeViScope server (GSCServer) connection. **Schema**: ```python 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**: ```json { "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**: ```python 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**: ```json { "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**: ```python 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**: ```json { "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**: ```python 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**: ```python 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**: ```json { "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**: ```python 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**: ```json { "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