feat: Geutebruck GeViScope/GeViSoft Action Mapping System - MVP
This MVP release provides a complete full-stack solution for managing action mappings in Geutebruck's GeViScope and GeViSoft video surveillance systems. ## Features ### Flutter Web Application (Port 8081) - Modern, responsive UI for managing action mappings - Action picker dialog with full parameter configuration - Support for both GSC (GeViScope) and G-Core server actions - Consistent UI for input and output actions with edit/delete capabilities - Real-time action mapping creation, editing, and deletion - Server categorization (GSC: prefix for GeViScope, G-Core: prefix for G-Core servers) ### FastAPI REST Backend (Port 8000) - RESTful API for action mapping CRUD operations - Action template service with comprehensive action catalog (247 actions) - Server management (G-Core and GeViScope servers) - Configuration tree reading and writing - JWT authentication with role-based access control - PostgreSQL database integration ### C# SDK Bridge (gRPC, Port 50051) - Native integration with GeViSoft SDK (GeViProcAPINET_4_0.dll) - Action mapping creation with correct binary format - Support for GSC and G-Core action types - Proper Camera parameter inclusion in action strings (fixes CrossSwitch bug) - Action ID lookup table with server-specific action IDs - Configuration reading/writing via SetupClient ## Bug Fixes - **CrossSwitch Bug**: GSC and G-Core actions now correctly display camera/PTZ head parameters in GeViSet - Action strings now include Camera parameter: `@ PanLeft (Comment: "", Camera: 101028)` - Proper filter flags and VideoInput=0 for action mappings - Correct action ID assignment (4198 for GSC, 9294 for G-Core PanLeft) ## Technical Stack - **Frontend**: Flutter Web, Dart, Dio HTTP client - **Backend**: Python FastAPI, PostgreSQL, Redis - **SDK Bridge**: C# .NET 8.0, gRPC, GeViSoft SDK - **Authentication**: JWT tokens - **Configuration**: GeViSoft .set files (binary format) ## Credentials - GeViSoft/GeViScope: username=sysadmin, password=masterkey - Default admin: username=admin, password=admin123 ## Deployment All services run on localhost: - Flutter Web: http://localhost:8081 - FastAPI: http://localhost:8000 - SDK Bridge gRPC: localhost:50051 - GeViServer: localhost (default port) Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
229
geutebruck-api/src/api/services/monitor_service.py
Normal file
229
geutebruck-api/src/api/services/monitor_service.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
Monitor service for managing monitor discovery and information
|
||||
"""
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
import structlog
|
||||
|
||||
from clients.sdk_bridge_client import sdk_bridge_client
|
||||
from clients.redis_client import redis_client
|
||||
from config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Redis cache TTL for monitor data (60 seconds)
|
||||
MONITOR_CACHE_TTL = 60
|
||||
|
||||
|
||||
class MonitorService:
|
||||
"""Service for monitor operations"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize monitor service"""
|
||||
pass
|
||||
|
||||
async def list_monitors(self, use_cache: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
Get list of all monitors from SDK Bridge
|
||||
|
||||
Args:
|
||||
use_cache: Whether to use Redis cache (default: True)
|
||||
|
||||
Returns:
|
||||
Dictionary with 'monitors' list and 'total' count
|
||||
"""
|
||||
cache_key = "monitors:list"
|
||||
|
||||
# Try to get from cache first
|
||||
if use_cache:
|
||||
cached_data = await redis_client.get_json(cache_key)
|
||||
if cached_data:
|
||||
logger.info("monitor_list_cache_hit")
|
||||
return cached_data
|
||||
|
||||
logger.info("monitor_list_cache_miss_fetching_from_sdk")
|
||||
|
||||
try:
|
||||
# Fetch monitors from SDK Bridge via gRPC
|
||||
monitors = await sdk_bridge_client.list_monitors()
|
||||
|
||||
# Transform to response format
|
||||
result = {
|
||||
"monitors": monitors,
|
||||
"total": len(monitors)
|
||||
}
|
||||
|
||||
# Cache the result
|
||||
if use_cache:
|
||||
await redis_client.set_json(cache_key, result, expire=MONITOR_CACHE_TTL)
|
||||
logger.info("monitor_list_cached", count=len(monitors), ttl=MONITOR_CACHE_TTL)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error("monitor_list_failed", error=str(e), exc_info=True)
|
||||
# Return empty list on error
|
||||
return {"monitors": [], "total": 0}
|
||||
|
||||
async def get_monitor(self, monitor_id: int, use_cache: bool = True) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get single monitor by ID
|
||||
|
||||
Args:
|
||||
monitor_id: Monitor ID (output channel number)
|
||||
use_cache: Whether to use Redis cache (default: True)
|
||||
|
||||
Returns:
|
||||
Monitor dictionary or None if not found
|
||||
"""
|
||||
cache_key = f"monitors:detail:{monitor_id}"
|
||||
|
||||
# Try to get from cache first
|
||||
if use_cache:
|
||||
cached_data = await redis_client.get_json(cache_key)
|
||||
if cached_data:
|
||||
logger.info("monitor_detail_cache_hit", monitor_id=monitor_id)
|
||||
return cached_data
|
||||
|
||||
logger.info("monitor_detail_cache_miss_fetching_from_sdk", monitor_id=monitor_id)
|
||||
|
||||
try:
|
||||
# Fetch monitor from SDK Bridge via gRPC
|
||||
monitor = await sdk_bridge_client.get_monitor(monitor_id)
|
||||
|
||||
if not monitor:
|
||||
logger.warning("monitor_not_found", monitor_id=monitor_id)
|
||||
return None
|
||||
|
||||
# Cache the result
|
||||
if use_cache:
|
||||
await redis_client.set_json(cache_key, monitor, expire=MONITOR_CACHE_TTL)
|
||||
logger.info("monitor_detail_cached", monitor_id=monitor_id, ttl=MONITOR_CACHE_TTL)
|
||||
|
||||
return monitor
|
||||
|
||||
except Exception as e:
|
||||
logger.error("monitor_detail_failed", monitor_id=monitor_id, error=str(e), exc_info=True)
|
||||
return None
|
||||
|
||||
async def invalidate_cache(self, monitor_id: Optional[int] = None) -> None:
|
||||
"""
|
||||
Invalidate monitor cache
|
||||
|
||||
Args:
|
||||
monitor_id: Specific monitor ID to invalidate, or None to invalidate all
|
||||
"""
|
||||
if monitor_id is not None:
|
||||
# Invalidate specific monitor
|
||||
cache_key = f"monitors:detail:{monitor_id}"
|
||||
await redis_client.delete(cache_key)
|
||||
logger.info("monitor_cache_invalidated", monitor_id=monitor_id)
|
||||
else:
|
||||
# Invalidate monitor list cache
|
||||
await redis_client.delete("monitors:list")
|
||||
logger.info("monitor_list_cache_invalidated")
|
||||
|
||||
async def refresh_monitor_list(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Force refresh monitor list from SDK Bridge (bypass cache)
|
||||
|
||||
Returns:
|
||||
Dictionary with 'monitors' list and 'total' count
|
||||
"""
|
||||
logger.info("monitor_list_force_refresh")
|
||||
|
||||
# Invalidate cache first
|
||||
await self.invalidate_cache()
|
||||
|
||||
# Fetch fresh data
|
||||
return await self.list_monitors(use_cache=False)
|
||||
|
||||
async def get_monitor_count(self) -> int:
|
||||
"""
|
||||
Get total number of monitors
|
||||
|
||||
Returns:
|
||||
Total monitor count
|
||||
"""
|
||||
result = await self.list_monitors(use_cache=True)
|
||||
return result["total"]
|
||||
|
||||
async def search_monitors(self, query: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search monitors by name or description
|
||||
|
||||
Args:
|
||||
query: Search query string
|
||||
|
||||
Returns:
|
||||
List of matching monitors
|
||||
"""
|
||||
result = await self.list_monitors(use_cache=True)
|
||||
monitors = result["monitors"]
|
||||
|
||||
# Simple case-insensitive search
|
||||
query_lower = query.lower()
|
||||
matching = [
|
||||
mon for mon in monitors
|
||||
if query_lower in mon.get("name", "").lower()
|
||||
or query_lower in mon.get("description", "").lower()
|
||||
]
|
||||
|
||||
logger.info("monitor_search", query=query, matches=len(matching))
|
||||
return matching
|
||||
|
||||
async def get_available_monitors(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get list of available (idle/free) monitors
|
||||
|
||||
Returns:
|
||||
List of monitors with no camera assigned
|
||||
"""
|
||||
result = await self.list_monitors(use_cache=True)
|
||||
monitors = result["monitors"]
|
||||
|
||||
# Available monitors have no camera assigned (current_camera_id is None or 0)
|
||||
available = [
|
||||
mon for mon in monitors
|
||||
if mon.get("current_camera_id") is None or mon.get("current_camera_id") == 0
|
||||
]
|
||||
|
||||
logger.info("available_monitors_retrieved", count=len(available), total=len(monitors))
|
||||
return available
|
||||
|
||||
async def get_active_monitors(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get list of active monitors (displaying a camera)
|
||||
|
||||
Returns:
|
||||
List of monitors with a camera assigned
|
||||
"""
|
||||
result = await self.list_monitors(use_cache=True)
|
||||
monitors = result["monitors"]
|
||||
|
||||
# Active monitors have a camera assigned
|
||||
active = [
|
||||
mon for mon in monitors
|
||||
if mon.get("current_camera_id") is not None and mon.get("current_camera_id") != 0
|
||||
]
|
||||
|
||||
logger.info("active_monitors_retrieved", count=len(active), total=len(monitors))
|
||||
return active
|
||||
|
||||
async def get_monitor_routing(self) -> Dict[int, Optional[int]]:
|
||||
"""
|
||||
Get current routing state (monitor_id -> camera_id mapping)
|
||||
|
||||
Returns:
|
||||
Dictionary mapping monitor IDs to current camera IDs
|
||||
"""
|
||||
result = await self.list_monitors(use_cache=True)
|
||||
monitors = result["monitors"]
|
||||
|
||||
routing = {
|
||||
mon["id"]: mon.get("current_camera_id")
|
||||
for mon in monitors
|
||||
}
|
||||
|
||||
logger.info("monitor_routing_retrieved", monitors=len(routing))
|
||||
return routing
|
||||
Reference in New Issue
Block a user