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>
This commit is contained in:
768
specs/001-surveillance-api/data-model.md
Normal file
768
specs/001-surveillance-api/data-model.md
Normal file
@@ -0,0 +1,768 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user