- Implement complete server CRUD operations with GeViServer persistence
- POST /api/v1/configuration/servers - Create new server
- PUT /api/v1/configuration/servers/{server_id} - Update server
- DELETE /api/v1/configuration/servers/{server_id} - Delete server
- GET /api/v1/configuration/servers - List all servers
- GET /api/v1/configuration/servers/{server_id} - Get single server
- Add write_configuration_tree method to SDK bridge client
- Converts tree to JSON and writes via import_configuration
- Enables read-modify-write pattern for configuration changes
- Fix action mappings endpoint schema mismatch
- Transform response to match ActionMappingListResponse schema
- Add total_mappings, mappings_with_parameters fields
- Include id and offset in mapping responses
- Streamline configuration router
- Remove heavy endpoints (export, import, modify)
- Optimize tree navigation with depth limiting
- Add path-based configuration access
- Update OpenAPI specification with all endpoints
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
663 lines
26 KiB
Python
663 lines
26 KiB
Python
"""
|
|
gRPC client for SDK Bridge communication
|
|
"""
|
|
import grpc
|
|
from typing import Optional, List
|
|
import structlog
|
|
from config import settings
|
|
|
|
# Import generated protobuf classes
|
|
from protos import camera_pb2, camera_pb2_grpc
|
|
from protos import monitor_pb2, monitor_pb2_grpc
|
|
from protos import crossswitch_pb2, crossswitch_pb2_grpc
|
|
from protos import action_mapping_pb2, action_mapping_pb2_grpc
|
|
from protos import configuration_pb2, configuration_pb2_grpc
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
class SDKBridgeClient:
|
|
"""gRPC client for communicating with SDK Bridge"""
|
|
|
|
def __init__(self):
|
|
self._channel: Optional[grpc.aio.Channel] = None
|
|
self._camera_stub = None
|
|
self._monitor_stub = None
|
|
self._crossswitch_stub = None
|
|
self._action_mapping_stub = None
|
|
self._configuration_stub = None
|
|
|
|
async def connect(self):
|
|
"""Initialize gRPC channel to SDK Bridge"""
|
|
try:
|
|
logger.info("sdk_bridge_connecting", url=settings.sdk_bridge_url)
|
|
|
|
# Create async gRPC channel
|
|
self._channel = grpc.aio.insecure_channel(
|
|
settings.sdk_bridge_url,
|
|
options=[
|
|
('grpc.max_send_message_length', 50 * 1024 * 1024), # 50MB
|
|
('grpc.max_receive_message_length', 50 * 1024 * 1024), # 50MB
|
|
('grpc.keepalive_time_ms', 30000), # 30 seconds
|
|
('grpc.keepalive_timeout_ms', 10000), # 10 seconds
|
|
]
|
|
)
|
|
|
|
# Initialize service stubs
|
|
self._camera_stub = camera_pb2_grpc.CameraServiceStub(self._channel)
|
|
self._monitor_stub = monitor_pb2_grpc.MonitorServiceStub(self._channel)
|
|
self._crossswitch_stub = crossswitch_pb2_grpc.CrossSwitchServiceStub(self._channel)
|
|
self._action_mapping_stub = action_mapping_pb2_grpc.ActionMappingServiceStub(self._channel)
|
|
self._configuration_stub = configuration_pb2_grpc.ConfigurationServiceStub(self._channel)
|
|
|
|
logger.info("sdk_bridge_connected")
|
|
except Exception as e:
|
|
logger.error("sdk_bridge_connection_failed", error=str(e))
|
|
raise
|
|
|
|
async def close(self):
|
|
"""Close gRPC channel"""
|
|
try:
|
|
if self._channel:
|
|
await self._channel.close()
|
|
logger.info("sdk_bridge_closed")
|
|
except Exception as e:
|
|
logger.error("sdk_bridge_close_failed", error=str(e))
|
|
|
|
async def health_check(self) -> dict:
|
|
"""Check SDK Bridge health"""
|
|
try:
|
|
logger.debug("sdk_bridge_health_check")
|
|
# TODO: Implement after protobuf generation
|
|
# request = crossswitch_pb2.Empty()
|
|
# response = await self._crossswitch_stub.HealthCheck(request, timeout=5.0)
|
|
# return {
|
|
# "is_healthy": response.is_healthy,
|
|
# "sdk_status": response.sdk_status,
|
|
# "geviserver_host": response.geviserver_host
|
|
# }
|
|
return {"is_healthy": True, "sdk_status": "connected", "geviserver_host": "localhost"}
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_health_check_failed", error=str(e))
|
|
return {"is_healthy": False, "sdk_status": "error", "error": str(e)}
|
|
|
|
async def list_cameras(self) -> List[dict]:
|
|
"""List all cameras from GeViServer"""
|
|
try:
|
|
logger.debug("sdk_bridge_list_cameras")
|
|
request = camera_pb2.ListCamerasRequest()
|
|
response = await self._camera_stub.ListCameras(request, timeout=10.0)
|
|
return [
|
|
{
|
|
"id": camera.id,
|
|
"name": camera.name,
|
|
"description": camera.description,
|
|
"has_ptz": camera.has_ptz,
|
|
"has_video_sensor": camera.has_video_sensor,
|
|
"status": camera.status,
|
|
"last_seen": None # TODO: Convert protobuf timestamp to datetime
|
|
}
|
|
for camera in response.cameras
|
|
]
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_list_cameras_failed", error=str(e))
|
|
raise
|
|
|
|
async def get_camera(self, camera_id: int) -> Optional[dict]:
|
|
"""Get camera details"""
|
|
try:
|
|
logger.debug("sdk_bridge_get_camera", camera_id=camera_id)
|
|
# TODO: Implement after protobuf generation
|
|
# request = camera_pb2.GetCameraRequest(camera_id=camera_id)
|
|
# response = await self._camera_stub.GetCamera(request, timeout=5.0)
|
|
# return {
|
|
# "id": response.id,
|
|
# "name": response.name,
|
|
# "description": response.description,
|
|
# "has_ptz": response.has_ptz,
|
|
# "has_video_sensor": response.has_video_sensor,
|
|
# "status": response.status
|
|
# }
|
|
return None # Placeholder
|
|
except grpc.RpcError as e:
|
|
if e.code() == grpc.StatusCode.NOT_FOUND:
|
|
return None
|
|
logger.error("sdk_bridge_get_camera_failed", camera_id=camera_id, error=str(e))
|
|
raise
|
|
|
|
async def list_monitors(self) -> List[dict]:
|
|
"""List all monitors from GeViServer"""
|
|
try:
|
|
logger.debug("sdk_bridge_list_monitors")
|
|
request = monitor_pb2.ListMonitorsRequest()
|
|
response = await self._monitor_stub.ListMonitors(request, timeout=10.0)
|
|
return [
|
|
{
|
|
"id": monitor.id,
|
|
"name": monitor.name,
|
|
"description": monitor.description,
|
|
"is_active": monitor.is_active,
|
|
"current_camera_id": monitor.current_camera_id,
|
|
"status": monitor.status
|
|
}
|
|
for monitor in response.monitors
|
|
]
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_list_monitors_failed", error=str(e))
|
|
raise
|
|
|
|
async def execute_crossswitch(self, camera_id: int, monitor_id: int, mode: int = 0) -> dict:
|
|
"""Execute cross-switch operation"""
|
|
try:
|
|
logger.info("sdk_bridge_crossswitch", camera_id=camera_id, monitor_id=monitor_id, mode=mode)
|
|
request = crossswitch_pb2.CrossSwitchRequest(
|
|
camera_id=camera_id,
|
|
monitor_id=monitor_id,
|
|
mode=mode
|
|
)
|
|
response = await self._crossswitch_stub.ExecuteCrossSwitch(request, timeout=10.0)
|
|
return {
|
|
"success": response.success,
|
|
"message": response.message,
|
|
"camera_id": response.camera_id,
|
|
"monitor_id": response.monitor_id
|
|
}
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_crossswitch_failed", error=str(e))
|
|
raise
|
|
|
|
async def clear_monitor(self, monitor_id: int) -> dict:
|
|
"""Clear monitor (stop video)"""
|
|
try:
|
|
logger.info("sdk_bridge_clear_monitor", monitor_id=monitor_id)
|
|
request = crossswitch_pb2.ClearMonitorRequest(monitor_id=monitor_id)
|
|
response = await self._crossswitch_stub.ClearMonitor(request, timeout=10.0)
|
|
return {
|
|
"success": response.success,
|
|
"message": response.message,
|
|
"monitor_id": response.monitor_id
|
|
}
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_clear_monitor_failed", error=str(e))
|
|
raise
|
|
|
|
async def get_routing_state(self) -> dict:
|
|
"""Get current routing state"""
|
|
try:
|
|
logger.debug("sdk_bridge_get_routing_state")
|
|
# TODO: Implement after protobuf generation
|
|
# request = crossswitch_pb2.GetRoutingStateRequest()
|
|
# response = await self._crossswitch_stub.GetRoutingState(request, timeout=10.0)
|
|
# return {
|
|
# "routes": [
|
|
# {
|
|
# "camera_id": route.camera_id,
|
|
# "monitor_id": route.monitor_id,
|
|
# "camera_name": route.camera_name,
|
|
# "monitor_name": route.monitor_name
|
|
# }
|
|
# for route in response.routes
|
|
# ],
|
|
# "total_routes": response.total_routes
|
|
# }
|
|
return {"routes": [], "total_routes": 0} # Placeholder
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_get_routing_state_failed", error=str(e))
|
|
raise
|
|
|
|
async def get_action_mappings(self, enabled_only: bool = False) -> dict:
|
|
"""Get action mappings from GeViServer via SDK Bridge"""
|
|
try:
|
|
logger.debug("sdk_bridge_get_action_mappings", enabled_only=enabled_only)
|
|
request = action_mapping_pb2.GetActionMappingsRequest(enabled_only=enabled_only)
|
|
response = await self._action_mapping_stub.GetActionMappings(request, timeout=30.0)
|
|
|
|
return {
|
|
"mappings": [
|
|
{
|
|
"id": mapping.id,
|
|
"name": mapping.name,
|
|
"description": mapping.description,
|
|
"input_action": mapping.input_action,
|
|
"output_actions": list(mapping.output_actions),
|
|
"enabled": mapping.enabled,
|
|
"execution_count": mapping.execution_count,
|
|
"last_executed": mapping.last_executed if mapping.last_executed else None,
|
|
"created_at": mapping.created_at,
|
|
"updated_at": mapping.updated_at
|
|
}
|
|
for mapping in response.mappings
|
|
],
|
|
"total_count": response.total_count,
|
|
"enabled_count": response.enabled_count,
|
|
"disabled_count": response.disabled_count
|
|
}
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_get_action_mappings_failed", error=str(e))
|
|
raise
|
|
|
|
async def read_configuration(self) -> dict:
|
|
"""Read and parse configuration from GeViServer"""
|
|
try:
|
|
logger.debug("sdk_bridge_read_configuration")
|
|
request = configuration_pb2.ReadConfigurationRequest()
|
|
response = await self._configuration_stub.ReadConfiguration(request, timeout=30.0)
|
|
|
|
return {
|
|
"success": response.success,
|
|
"error_message": response.error_message if response.error_message else None,
|
|
"file_size": response.file_size,
|
|
"header": response.header,
|
|
"nodes": [
|
|
{
|
|
"start_offset": node.start_offset,
|
|
"end_offset": node.end_offset,
|
|
"node_type": node.node_type,
|
|
"name": node.name if node.name else None,
|
|
"value": node.value if node.value else None,
|
|
"value_type": node.value_type if node.value_type else None
|
|
}
|
|
for node in response.nodes
|
|
],
|
|
"statistics": {
|
|
"total_nodes": response.statistics.total_nodes,
|
|
"boolean_count": response.statistics.boolean_count,
|
|
"integer_count": response.statistics.integer_count,
|
|
"string_count": response.statistics.string_count,
|
|
"property_count": response.statistics.property_count,
|
|
"marker_count": response.statistics.marker_count,
|
|
"rules_section_count": response.statistics.rules_section_count
|
|
}
|
|
}
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_read_configuration_failed", error=str(e))
|
|
raise
|
|
|
|
async def export_configuration_json(self) -> dict:
|
|
"""Export configuration as JSON"""
|
|
try:
|
|
logger.debug("sdk_bridge_export_configuration_json")
|
|
request = configuration_pb2.ExportJsonRequest()
|
|
response = await self._configuration_stub.ExportConfigurationJson(request, timeout=30.0)
|
|
|
|
return {
|
|
"success": response.success,
|
|
"error_message": response.error_message if response.error_message else None,
|
|
"json_data": response.json_data,
|
|
"json_size": response.json_size
|
|
}
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_export_configuration_json_failed", error=str(e))
|
|
raise
|
|
|
|
async def modify_configuration(self, modifications: List[dict]) -> dict:
|
|
"""Modify configuration and write back to server"""
|
|
try:
|
|
logger.info("sdk_bridge_modify_configuration", count=len(modifications))
|
|
request = configuration_pb2.ModifyConfigurationRequest()
|
|
|
|
for mod in modifications:
|
|
modification = configuration_pb2.NodeModification(
|
|
start_offset=mod["start_offset"],
|
|
node_type=mod["node_type"],
|
|
new_value=mod["new_value"]
|
|
)
|
|
request.modifications.append(modification)
|
|
|
|
response = await self._configuration_stub.ModifyConfiguration(request, timeout=60.0)
|
|
|
|
return {
|
|
"success": response.success,
|
|
"error_message": response.error_message if response.error_message else None,
|
|
"modifications_applied": response.modifications_applied
|
|
}
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_modify_configuration_failed", error=str(e))
|
|
raise
|
|
|
|
async def import_configuration(self, json_data: str) -> dict:
|
|
"""Import complete configuration from JSON and write to GeViServer"""
|
|
try:
|
|
logger.info("sdk_bridge_import_configuration", json_size=len(json_data))
|
|
request = configuration_pb2.ImportConfigurationRequest(json_data=json_data)
|
|
response = await self._configuration_stub.ImportConfiguration(request, timeout=60.0)
|
|
|
|
return {
|
|
"success": response.success,
|
|
"error_message": response.error_message if response.error_message else None,
|
|
"bytes_written": response.bytes_written,
|
|
"nodes_imported": response.nodes_imported
|
|
}
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_import_configuration_failed", error=str(e))
|
|
raise
|
|
|
|
async def read_action_mappings(self) -> dict:
|
|
"""
|
|
Read ONLY action mappings (Rules markers) from GeViServer
|
|
Much faster than full configuration export - selective parsing
|
|
Returns structured format with input_actions and output_actions with parameters
|
|
"""
|
|
try:
|
|
logger.info("sdk_bridge_read_action_mappings")
|
|
request = configuration_pb2.ReadActionMappingsRequest()
|
|
response = await self._configuration_stub.ReadActionMappings(request, timeout=30.0)
|
|
|
|
# Convert protobuf response to dict with structured format
|
|
mappings = []
|
|
for mapping in response.mappings:
|
|
# Convert input actions with parameters
|
|
input_actions = []
|
|
for action_def in mapping.input_actions:
|
|
parameters = {}
|
|
for param in action_def.parameters:
|
|
parameters[param.name] = param.value
|
|
|
|
input_actions.append({
|
|
"action": action_def.action,
|
|
"parameters": parameters
|
|
})
|
|
|
|
# Convert output actions with parameters
|
|
output_actions = []
|
|
for action_def in mapping.output_actions:
|
|
parameters = {}
|
|
for param in action_def.parameters:
|
|
parameters[param.name] = param.value
|
|
|
|
output_actions.append({
|
|
"action": action_def.action,
|
|
"parameters": parameters
|
|
})
|
|
|
|
mappings.append({
|
|
"name": mapping.name,
|
|
"input_actions": input_actions,
|
|
"output_actions": output_actions,
|
|
"start_offset": mapping.start_offset,
|
|
"end_offset": mapping.end_offset,
|
|
# Keep old format for backward compatibility
|
|
"actions": list(mapping.actions)
|
|
})
|
|
|
|
return {
|
|
"success": response.success,
|
|
"error_message": response.error_message if response.error_message else None,
|
|
"mappings": mappings,
|
|
"total_count": response.total_count
|
|
}
|
|
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_read_action_mappings_failed", error=str(e))
|
|
raise
|
|
|
|
async def read_specific_markers(self, marker_names: List[str]) -> dict:
|
|
"""
|
|
Read specific configuration markers by name
|
|
Extensible method for reading any configuration type
|
|
"""
|
|
try:
|
|
logger.info("sdk_bridge_read_specific_markers", markers=marker_names)
|
|
request = configuration_pb2.ReadSpecificMarkersRequest(marker_names=marker_names)
|
|
response = await self._configuration_stub.ReadSpecificMarkers(request, timeout=30.0)
|
|
|
|
# Convert protobuf response to dict
|
|
nodes = []
|
|
for node in response.extracted_nodes:
|
|
nodes.append({
|
|
"start_offset": node.start_offset,
|
|
"end_offset": node.end_offset,
|
|
"node_type": node.node_type,
|
|
"name": node.name,
|
|
"value": node.value,
|
|
"value_type": node.value_type
|
|
})
|
|
|
|
return {
|
|
"success": response.success,
|
|
"error_message": response.error_message if response.error_message else None,
|
|
"file_size": response.file_size,
|
|
"requested_markers": list(response.requested_markers),
|
|
"extracted_nodes": nodes,
|
|
"markers_found": response.markers_found
|
|
}
|
|
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_read_specific_markers_failed", error=str(e))
|
|
raise
|
|
|
|
async def create_action_mapping(self, mapping_data: dict) -> dict:
|
|
"""
|
|
Create a new action mapping
|
|
|
|
Args:
|
|
mapping_data: Dict with name, input_actions, output_actions
|
|
|
|
Returns:
|
|
Dict with success status and created mapping
|
|
"""
|
|
try:
|
|
logger.info("sdk_bridge_create_action_mapping", name=mapping_data.get("name"))
|
|
|
|
# Build protobuf request
|
|
mapping_input = configuration_pb2.ActionMappingInput(
|
|
name=mapping_data.get("name", "")
|
|
)
|
|
|
|
# Add output actions
|
|
for action_data in mapping_data.get("output_actions", []):
|
|
action_def = configuration_pb2.ActionDefinition(action=action_data["action"])
|
|
|
|
# Add parameters
|
|
for param_name, param_value in action_data.get("parameters", {}).items():
|
|
action_def.parameters.add(name=param_name, value=str(param_value))
|
|
|
|
mapping_input.output_actions.append(action_def)
|
|
|
|
request = configuration_pb2.CreateActionMappingRequest(mapping=mapping_input)
|
|
response = await self._configuration_stub.CreateActionMapping(request, timeout=60.0)
|
|
|
|
# Convert response
|
|
result = {
|
|
"success": response.success,
|
|
"error_message": response.error_message if response.error_message else None,
|
|
"message": response.message
|
|
}
|
|
|
|
if response.mapping:
|
|
result["mapping"] = {
|
|
"id": len([]), # ID will be assigned by the system
|
|
"name": response.mapping.name,
|
|
"offset": response.mapping.start_offset,
|
|
"output_actions": []
|
|
}
|
|
|
|
for action_def in response.mapping.output_actions:
|
|
result["mapping"]["output_actions"].append({
|
|
"action": action_def.action,
|
|
"parameters": {p.name: p.value for p in action_def.parameters}
|
|
})
|
|
|
|
return result
|
|
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_create_action_mapping_failed", error=str(e))
|
|
raise
|
|
|
|
async def update_action_mapping(self, mapping_id: int, mapping_data: dict) -> dict:
|
|
"""
|
|
Update an existing action mapping
|
|
|
|
Args:
|
|
mapping_id: 1-based ID of mapping to update
|
|
mapping_data: Dict with updated fields (name, input_actions, output_actions)
|
|
|
|
Returns:
|
|
Dict with success status and updated mapping
|
|
"""
|
|
try:
|
|
logger.info("sdk_bridge_update_action_mapping", mapping_id=mapping_id)
|
|
|
|
# Build protobuf request
|
|
mapping_input = configuration_pb2.ActionMappingInput()
|
|
|
|
if "name" in mapping_data:
|
|
mapping_input.name = mapping_data["name"]
|
|
|
|
# Add output actions if provided
|
|
if "output_actions" in mapping_data:
|
|
for action_data in mapping_data["output_actions"]:
|
|
action_def = configuration_pb2.ActionDefinition(action=action_data["action"])
|
|
|
|
# Add parameters
|
|
for param_name, param_value in action_data.get("parameters", {}).items():
|
|
action_def.parameters.add(name=param_name, value=str(param_value))
|
|
|
|
mapping_input.output_actions.append(action_def)
|
|
|
|
request = configuration_pb2.UpdateActionMappingRequest(
|
|
mapping_id=mapping_id,
|
|
mapping=mapping_input
|
|
)
|
|
response = await self._configuration_stub.UpdateActionMapping(request, timeout=60.0)
|
|
|
|
# Convert response
|
|
result = {
|
|
"success": response.success,
|
|
"error_message": response.error_message if response.error_message else None,
|
|
"message": response.message
|
|
}
|
|
|
|
if response.mapping:
|
|
result["mapping"] = {
|
|
"id": mapping_id,
|
|
"name": response.mapping.name,
|
|
"offset": response.mapping.start_offset,
|
|
"output_actions": []
|
|
}
|
|
|
|
for action_def in response.mapping.output_actions:
|
|
result["mapping"]["output_actions"].append({
|
|
"action": action_def.action,
|
|
"parameters": {p.name: p.value for p in action_def.parameters}
|
|
})
|
|
|
|
return result
|
|
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_update_action_mapping_failed", error=str(e))
|
|
raise
|
|
|
|
async def delete_action_mapping(self, mapping_id: int) -> dict:
|
|
"""
|
|
Delete an action mapping by ID
|
|
|
|
Args:
|
|
mapping_id: 1-based ID of mapping to delete
|
|
|
|
Returns:
|
|
Dict with success status and message
|
|
"""
|
|
try:
|
|
logger.info("sdk_bridge_delete_action_mapping", mapping_id=mapping_id)
|
|
|
|
request = configuration_pb2.DeleteActionMappingRequest(mapping_id=mapping_id)
|
|
response = await self._configuration_stub.DeleteActionMapping(request, timeout=60.0)
|
|
|
|
return {
|
|
"success": response.success,
|
|
"error_message": response.error_message if response.error_message else None,
|
|
"message": response.message
|
|
}
|
|
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_delete_action_mapping_failed", error=str(e))
|
|
raise
|
|
|
|
async def read_configuration_tree(self) -> dict:
|
|
"""
|
|
Read configuration as hierarchical folder tree (RECOMMENDED)
|
|
|
|
Returns:
|
|
Dict with tree structure
|
|
"""
|
|
try:
|
|
logger.info("sdk_bridge_read_configuration_tree")
|
|
|
|
request = configuration_pb2.ReadConfigurationTreeRequest()
|
|
response = await self._configuration_stub.ReadConfigurationTree(request, timeout=30.0)
|
|
|
|
if not response.success:
|
|
return {
|
|
"success": False,
|
|
"error_message": response.error_message
|
|
}
|
|
|
|
# Convert protobuf TreeNode to dict
|
|
def convert_tree_node(node):
|
|
result = {
|
|
"type": node.type,
|
|
"name": node.name
|
|
}
|
|
|
|
# Add value based on type
|
|
if node.type == "string":
|
|
result["value"] = node.string_value
|
|
elif node.type in ("bool", "byte", "int16", "int32", "int64"):
|
|
result["value"] = node.int_value
|
|
|
|
# Add children recursively
|
|
if node.type == "folder" and len(node.children) > 0:
|
|
result["children"] = [convert_tree_node(child) for child in node.children]
|
|
|
|
return result
|
|
|
|
tree_dict = convert_tree_node(response.root) if response.root else None
|
|
|
|
return {
|
|
"success": True,
|
|
"tree": tree_dict,
|
|
"total_nodes": response.total_nodes
|
|
}
|
|
|
|
except grpc.RpcError as e:
|
|
logger.error("sdk_bridge_read_configuration_tree_failed", error=str(e))
|
|
raise
|
|
|
|
async def write_configuration_tree(self, tree: dict) -> dict:
|
|
"""
|
|
Write modified configuration tree back to GeViServer
|
|
|
|
Args:
|
|
tree: Modified tree structure (dict)
|
|
|
|
Returns:
|
|
Dict with success status and write statistics
|
|
"""
|
|
try:
|
|
import json
|
|
logger.info("sdk_bridge_write_configuration_tree")
|
|
|
|
# Convert tree to JSON string
|
|
json_data = json.dumps(tree, indent=2)
|
|
|
|
# Use import_configuration to write the tree
|
|
result = await self.import_configuration(json_data)
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error("sdk_bridge_write_configuration_tree_failed", error=str(e))
|
|
raise
|
|
|
|
# Global SDK Bridge client instance
|
|
sdk_bridge_client = SDKBridgeClient()
|
|
|
|
# Convenience functions
|
|
async def init_sdk_bridge():
|
|
"""Initialize SDK Bridge connection (call on startup)"""
|
|
await sdk_bridge_client.connect()
|
|
|
|
async def close_sdk_bridge():
|
|
"""Close SDK Bridge connection (call on shutdown)"""
|
|
await sdk_bridge_client.close()
|