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>
489 lines
18 KiB
Python
489 lines
18 KiB
Python
# ============ SERVER CONFIGURATION (G-CORE & GEVISCOPE) ============
|
|
|
|
# Parent endpoint - List all servers (both types)
|
|
@router.get(
|
|
"/servers",
|
|
response_model=AllServersResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="List all servers (both G-Core and GeViScope)",
|
|
description="Get combined list of all server types"
|
|
)
|
|
async def list_all_servers(
|
|
current_user: User = Depends(require_viewer)
|
|
):
|
|
"""List all servers - both G-Core and GeViScope"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
# Get G-Core servers
|
|
gcore_servers = await _get_gcore_servers_list(service)
|
|
|
|
# Get GeViScope servers
|
|
geviscope_servers = await _get_geviscope_servers_list(service)
|
|
|
|
return AllServersResponse(
|
|
gcore_servers=gcore_servers,
|
|
geviscope_servers=geviscope_servers,
|
|
total_gcore=len(gcore_servers),
|
|
total_geviscope=len(geviscope_servers),
|
|
total=len(gcore_servers) + len(geviscope_servers)
|
|
)
|
|
except Exception as e:
|
|
logger.error("list_all_servers_error", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to list servers: {str(e)}"
|
|
)
|
|
|
|
|
|
# ============ G-CORE SERVERS ============
|
|
|
|
@router.get(
|
|
"/servers/gcore",
|
|
response_model=GCoreServerListResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="List G-Core servers",
|
|
description="Get all G-Core servers from GeViGCoreServer folder"
|
|
)
|
|
async def list_gcore_servers(
|
|
current_user: User = Depends(require_viewer)
|
|
):
|
|
"""List all G-Core servers"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
servers = await _get_gcore_servers_list(service)
|
|
return GCoreServerListResponse(servers=servers, total=len(servers))
|
|
except Exception as e:
|
|
logger.error("list_gcore_servers_error", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to list G-Core servers: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/servers/gcore/{server_id}",
|
|
response_model=GCoreServerResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="Get G-Core server",
|
|
description="Get details of a specific G-Core server by ID"
|
|
)
|
|
async def get_gcore_server(
|
|
server_id: str,
|
|
current_user: User = Depends(require_viewer)
|
|
):
|
|
"""Get single G-Core server by ID"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
server = await _get_gcore_server_by_id(service, server_id)
|
|
if not server:
|
|
raise ValueError(f"G-Core server '{server_id}' not found")
|
|
return server
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("get_gcore_server_error", server_id=server_id, error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to get G-Core server: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/servers/gcore",
|
|
response_model=ServerOperationResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create G-Core server",
|
|
description="Create a new G-Core server"
|
|
)
|
|
async def create_gcore_server(
|
|
server_data: GCoreServerCreate,
|
|
current_user: User = Depends(require_administrator)
|
|
):
|
|
"""Create new G-Core server"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
result = await service.create_server({
|
|
"alias": server_data.alias,
|
|
"host": server_data.host,
|
|
"user": server_data.user,
|
|
"password": server_data.password,
|
|
"enabled": server_data.enabled,
|
|
"deactivate_echo": server_data.deactivate_echo,
|
|
"deactivate_live_check": server_data.deactivate_live_check
|
|
})
|
|
|
|
return ServerOperationResponse(
|
|
success=result.get("success", False),
|
|
message=result.get("message", ""),
|
|
server_id=result.get("server_id")
|
|
)
|
|
except Exception as e:
|
|
logger.error("create_gcore_server_error", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to create G-Core server: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.put(
|
|
"/servers/gcore/{server_id}",
|
|
response_model=ServerOperationResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="Update G-Core server",
|
|
description="Update an existing G-Core server"
|
|
)
|
|
async def update_gcore_server(
|
|
server_id: str,
|
|
server_data: GCoreServerUpdate,
|
|
current_user: User = Depends(require_administrator)
|
|
):
|
|
"""Update existing G-Core server"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
result = await service.update_server(server_id, {
|
|
"alias": server_data.alias,
|
|
"host": server_data.host,
|
|
"user": server_data.user,
|
|
"password": server_data.password,
|
|
"enabled": server_data.enabled,
|
|
"deactivate_echo": server_data.deactivate_echo,
|
|
"deactivate_live_check": server_data.deactivate_live_check
|
|
})
|
|
|
|
return ServerOperationResponse(
|
|
success=result.get("success", False),
|
|
message=result.get("message", ""),
|
|
server_id=server_id
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("update_gcore_server_error", server_id=server_id, error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to update G-Core server: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.delete(
|
|
"/servers/gcore/{server_id}",
|
|
response_model=ServerOperationResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="Delete G-Core server",
|
|
description="Delete a G-Core server"
|
|
)
|
|
async def delete_gcore_server(
|
|
server_id: str,
|
|
current_user: User = Depends(require_administrator)
|
|
):
|
|
"""Delete G-Core server"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
result = await service.delete_server(server_id)
|
|
return ServerOperationResponse(
|
|
success=result.get("success", False),
|
|
message=result.get("message", ""),
|
|
server_id=server_id
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("delete_gcore_server_error", server_id=server_id, error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to delete G-Core server: {str(e)}"
|
|
)
|
|
|
|
|
|
# ============ GEVISCOPE SERVERS ============
|
|
|
|
@router.get(
|
|
"/servers/geviscope",
|
|
response_model=GeViScopeServerListResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="List GeViScope servers",
|
|
description="Get all GeViScope servers from GeViGscServer folder"
|
|
)
|
|
async def list_geviscope_servers(
|
|
current_user: User = Depends(require_viewer)
|
|
):
|
|
"""List all GeViScope servers"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
servers = await _get_geviscope_servers_list(service)
|
|
return GeViScopeServerListResponse(servers=servers, total=len(servers))
|
|
except Exception as e:
|
|
logger.error("list_geviscope_servers_error", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to list GeViScope servers: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/servers/geviscope/{server_id}",
|
|
response_model=GeViScopeServerResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="Get GeViScope server",
|
|
description="Get details of a specific GeViScope server by ID"
|
|
)
|
|
async def get_geviscope_server(
|
|
server_id: str,
|
|
current_user: User = Depends(require_viewer)
|
|
):
|
|
"""Get single GeViScope server by ID"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
server = await _get_geviscope_server_by_id(service, server_id)
|
|
if not server:
|
|
raise ValueError(f"GeViScope server '{server_id}' not found")
|
|
return server
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("get_geviscope_server_error", server_id=server_id, error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to get GeViScope server: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/servers/geviscope",
|
|
response_model=ServerOperationResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create GeViScope server",
|
|
description="Create a new GeViScope server"
|
|
)
|
|
async def create_geviscope_server(
|
|
server_data: GeViScopeServerCreate,
|
|
current_user: User = Depends(require_administrator)
|
|
):
|
|
"""Create new GeViScope server"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
result = await service.create_geviscope_server({
|
|
"alias": server_data.alias,
|
|
"host": server_data.host,
|
|
"user": server_data.user,
|
|
"password": server_data.password,
|
|
"enabled": server_data.enabled,
|
|
"deactivate_echo": server_data.deactivate_echo,
|
|
"deactivate_live_check": server_data.deactivate_live_check,
|
|
"dialup_broadcast_aware": server_data.dialup_broadcast_aware,
|
|
"dialup_connection": server_data.dialup_connection,
|
|
"dialup_cpa_connection": server_data.dialup_cpa_connection,
|
|
"dialup_cpa_connection_interval": server_data.dialup_cpa_connection_interval,
|
|
"dialup_cpa_time_settings": server_data.dialup_cpa_time_settings,
|
|
"dialup_keep_alive": server_data.dialup_keep_alive,
|
|
"dialup_keep_alive_retrigger": server_data.dialup_keep_alive_retrigger,
|
|
"dialup_keep_alive_time": server_data.dialup_keep_alive_time
|
|
})
|
|
|
|
return ServerOperationResponse(
|
|
success=result.get("success", False),
|
|
message=result.get("message", ""),
|
|
server_id=result.get("server_id")
|
|
)
|
|
except Exception as e:
|
|
logger.error("create_geviscope_server_error", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to create GeViScope server: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.put(
|
|
"/servers/geviscope/{server_id}",
|
|
response_model=ServerOperationResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="Update GeViScope server",
|
|
description="Update an existing GeViScope server"
|
|
)
|
|
async def update_geviscope_server(
|
|
server_id: str,
|
|
server_data: GeViScopeServerUpdate,
|
|
current_user: User = Depends(require_administrator)
|
|
):
|
|
"""Update existing GeViScope server"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
result = await service.update_geviscope_server(server_id, {
|
|
"alias": server_data.alias,
|
|
"host": server_data.host,
|
|
"user": server_data.user,
|
|
"password": server_data.password,
|
|
"enabled": server_data.enabled,
|
|
"deactivate_echo": server_data.deactivate_echo,
|
|
"deactivate_live_check": server_data.deactivate_live_check,
|
|
"dialup_broadcast_aware": server_data.dialup_broadcast_aware,
|
|
"dialup_connection": server_data.dialup_connection,
|
|
"dialup_cpa_connection": server_data.dialup_cpa_connection,
|
|
"dialup_cpa_connection_interval": server_data.dialup_cpa_connection_interval,
|
|
"dialup_cpa_time_settings": server_data.dialup_cpa_time_settings,
|
|
"dialup_keep_alive": server_data.dialup_keep_alive,
|
|
"dialup_keep_alive_retrigger": server_data.dialup_keep_alive_retrigger,
|
|
"dialup_keep_alive_time": server_data.dialup_keep_alive_time
|
|
})
|
|
|
|
return ServerOperationResponse(
|
|
success=result.get("success", False),
|
|
message=result.get("message", ""),
|
|
server_id=server_id
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("update_geviscope_server_error", server_id=server_id, error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to update GeViScope server: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.delete(
|
|
"/servers/geviscope/{server_id}",
|
|
response_model=ServerOperationResponse,
|
|
status_code=status.HTTP_200_OK,
|
|
summary="Delete GeViScope server",
|
|
description="Delete a GeViScope server"
|
|
)
|
|
async def delete_geviscope_server(
|
|
server_id: str,
|
|
current_user: User = Depends(require_administrator)
|
|
):
|
|
"""Delete GeViScope server"""
|
|
service = ConfigurationService()
|
|
|
|
try:
|
|
result = await service.delete_geviscope_server(server_id)
|
|
return ServerOperationResponse(
|
|
success=result.get("success", False),
|
|
message=result.get("message", ""),
|
|
server_id=server_id
|
|
)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("delete_geviscope_server_error", server_id=server_id, error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to delete GeViScope server: {str(e)}"
|
|
)
|
|
|
|
|
|
# ============ HELPER FUNCTIONS ============
|
|
|
|
async def _get_gcore_servers_list(service: ConfigurationService) -> List[GCoreServerResponse]:
|
|
"""Helper to get list of G-Core servers"""
|
|
gcore_folder = await service.read_configuration_path("GeViGCoreServer")
|
|
servers = []
|
|
|
|
if gcore_folder.get("type") == "folder" and "children" in gcore_folder:
|
|
for child in gcore_folder["children"]:
|
|
if child.get("type") != "folder":
|
|
continue
|
|
|
|
server = _parse_gcore_server(child)
|
|
if server:
|
|
servers.append(server)
|
|
|
|
return servers
|
|
|
|
|
|
async def _get_gcore_server_by_id(service: ConfigurationService, server_id: str) -> Optional[GCoreServerResponse]:
|
|
"""Helper to get single G-Core server by ID"""
|
|
gcore_folder = await service.read_configuration_path("GeViGCoreServer")
|
|
|
|
if gcore_folder.get("type") != "folder" or "children" not in gcore_folder:
|
|
return None
|
|
|
|
for child in gcore_folder["children"]:
|
|
if child.get("type") == "folder" and child.get("name") == server_id:
|
|
return _parse_gcore_server(child)
|
|
|
|
return None
|
|
|
|
|
|
def _parse_gcore_server(child: dict) -> Optional[GCoreServerResponse]:
|
|
"""Parse G-Core server from config tree node"""
|
|
server_id = child.get("name")
|
|
children_dict = {c.get("name"): c for c in child.get("children", [])}
|
|
|
|
return GCoreServerResponse(
|
|
id=server_id,
|
|
alias=children_dict.get("Alias", {}).get("value", ""),
|
|
host=children_dict.get("Host", {}).get("value", ""),
|
|
user=children_dict.get("User", {}).get("value", ""),
|
|
password=children_dict.get("Password", {}).get("value", ""),
|
|
enabled=bool(children_dict.get("Enabled", {}).get("value", 0)),
|
|
deactivate_echo=bool(children_dict.get("DeactivateEcho", {}).get("value", 0)),
|
|
deactivate_live_check=bool(children_dict.get("DeactivateLiveCheck", {}).get("value", 0))
|
|
)
|
|
|
|
|
|
async def _get_geviscope_servers_list(service: ConfigurationService) -> List[GeViScopeServerResponse]:
|
|
"""Helper to get list of GeViScope servers"""
|
|
gsc_folder = await service.read_configuration_path("GeViGscServer")
|
|
servers = []
|
|
|
|
if gsc_folder.get("type") == "folder" and "children" in gsc_folder:
|
|
for child in gsc_folder["children"]:
|
|
if child.get("type") != "folder":
|
|
continue # Skip global settings (non-folder items)
|
|
|
|
server = _parse_geviscope_server(child)
|
|
if server:
|
|
servers.append(server)
|
|
|
|
return servers
|
|
|
|
|
|
async def _get_geviscope_server_by_id(service: ConfigurationService, server_id: str) -> Optional[GeViScopeServerResponse]:
|
|
"""Helper to get single GeViScope server by ID"""
|
|
gsc_folder = await service.read_configuration_path("GeViGscServer")
|
|
|
|
if gsc_folder.get("type") != "folder" or "children" not in gsc_folder:
|
|
return None
|
|
|
|
for child in gsc_folder["children"]:
|
|
if child.get("type") == "folder" and child.get("name") == server_id:
|
|
return _parse_geviscope_server(child)
|
|
|
|
return None
|
|
|
|
|
|
def _parse_geviscope_server(child: dict) -> Optional[GeViScopeServerResponse]:
|
|
"""Parse GeViScope server from config tree node"""
|
|
server_id = child.get("name")
|
|
children_dict = {c.get("name"): c for c in child.get("children", [])}
|
|
|
|
return GeViScopeServerResponse(
|
|
id=server_id,
|
|
alias=children_dict.get("Alias", {}).get("value", ""),
|
|
host=children_dict.get("Host", {}).get("value", ""),
|
|
user=children_dict.get("User", {}).get("value", ""),
|
|
password=children_dict.get("Password", {}).get("value", ""),
|
|
enabled=bool(children_dict.get("Enabled", {}).get("value", 0)),
|
|
deactivate_echo=bool(children_dict.get("DeactivateEcho", {}).get("value", 0)),
|
|
deactivate_live_check=bool(children_dict.get("DeactivateLiveCheck", {}).get("value", 0)),
|
|
dialup_broadcast_aware=bool(children_dict.get("DialUpBroadcastAware", {}).get("value", 0)),
|
|
dialup_connection=bool(children_dict.get("DialUpConnection", {}).get("value", 0)),
|
|
dialup_cpa_connection=bool(children_dict.get("DialUpCPAConnection", {}).get("value", 0)),
|
|
dialup_cpa_connection_interval=int(children_dict.get("DialUpCPAConnectionInterval", {}).get("value", 3600)),
|
|
dialup_cpa_time_settings=int(children_dict.get("DialUpCPATimeSettings", {}).get("value", 16777215)),
|
|
dialup_keep_alive=bool(children_dict.get("DialUpKeepAlive", {}).get("value", 0)),
|
|
dialup_keep_alive_retrigger=bool(children_dict.get("DialUpKeepAliveRetrigger", {}).get("value", 0)),
|
|
dialup_keep_alive_time=int(children_dict.get("DialUpKeepAliveTime", {}).get("value", 10))
|
|
)
|