CalejoControl/src/dashboard/api.py

803 lines
31 KiB
Python

"""
Dashboard API for Calejo Control Adapter
Provides REST endpoints for configuration management and system monitoring
"""
import json
import logging
from typing import Dict, Any, List, Optional
from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, ValidationError
from config.settings import Settings
from .configuration_manager import (
configuration_manager, OPCUAConfig, ModbusTCPConfig, PumpStationConfig,
PumpConfig, SafetyLimitsConfig, DataPointMapping, ProtocolType
)
from datetime import datetime
logger = logging.getLogger(__name__)
# API Router
dashboard_router = APIRouter(prefix="/api/v1/dashboard", tags=["dashboard"])
# Pydantic models for configuration
class DatabaseConfig(BaseModel):
db_host: str = "localhost"
db_port: int = 5432
db_name: str = "calejo"
db_user: str = "calejo"
db_password: str = ""
class OPCUAConfig(BaseModel):
enabled: bool = True
host: str = "localhost"
port: int = 4840
class ModbusConfig(BaseModel):
enabled: bool = True
host: str = "localhost"
port: int = 502
unit_id: int = 1
class RESTAPIConfig(BaseModel):
enabled: bool = True
host: str = "0.0.0.0"
port: int = 8080
cors_enabled: bool = True
class MonitoringConfig(BaseModel):
health_monitor_port: int = 9090
metrics_enabled: bool = True
class SecurityConfig(BaseModel):
jwt_secret_key: str = ""
api_key: str = ""
class SystemConfig(BaseModel):
database: DatabaseConfig
opcua: OPCUAConfig
modbus: ModbusConfig
rest_api: RESTAPIConfig
monitoring: MonitoringConfig
security: SecurityConfig
class ValidationResult(BaseModel):
valid: bool
errors: List[str] = []
warnings: List[str] = []
class DashboardStatus(BaseModel):
application_status: str
database_status: str
opcua_status: str
modbus_status: str
rest_api_status: str
monitoring_status: str
# Global settings instance
settings = Settings()
@dashboard_router.get("/config", response_model=SystemConfig)
async def get_configuration():
"""Get current system configuration"""
try:
config = SystemConfig(
database=DatabaseConfig(
db_host=settings.db_host,
db_port=settings.db_port,
db_name=settings.db_name,
db_user=settings.db_user,
db_password="********" # Don't expose actual password
),
opcua=OPCUAConfig(
enabled=settings.opcua_enabled,
host=settings.opcua_host,
port=settings.opcua_port
),
modbus=ModbusConfig(
enabled=settings.modbus_enabled,
host=settings.modbus_host,
port=settings.modbus_port,
unit_id=settings.modbus_unit_id
),
rest_api=RESTAPIConfig(
enabled=settings.rest_api_enabled,
host=settings.rest_api_host,
port=settings.rest_api_port,
cors_enabled=settings.rest_api_cors_enabled
),
monitoring=MonitoringConfig(
health_monitor_port=settings.health_monitor_port,
metrics_enabled=True
),
security=SecurityConfig(
jwt_secret_key="********",
api_key="********"
)
)
return config
except Exception as e:
logger.error(f"Error getting configuration: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to retrieve configuration")
@dashboard_router.post("/config", response_model=ValidationResult)
async def update_configuration(config: SystemConfig, background_tasks: BackgroundTasks):
"""Update system configuration"""
try:
# Validate configuration
validation_result = validate_configuration(config)
if validation_result.valid:
# Save configuration in background
background_tasks.add_task(save_configuration, config)
return validation_result
except Exception as e:
logger.error(f"Error updating configuration: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to update configuration")
@dashboard_router.get("/status", response_model=DashboardStatus)
async def get_system_status():
"""Get current system status"""
try:
# This would integrate with the health monitor
# For now, return mock status
status = DashboardStatus(
application_status="running",
database_status="connected",
opcua_status="listening",
modbus_status="listening",
rest_api_status="running",
monitoring_status="active"
)
return status
except Exception as e:
logger.error(f"Error getting system status: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to retrieve system status")
@dashboard_router.post("/restart")
async def restart_system():
"""Restart the system (admin only)"""
# This would trigger a system restart
# For now, just log the request
logger.info("System restart requested via dashboard")
return {"message": "Restart request received", "status": "pending"}
@dashboard_router.get("/backup")
async def create_backup():
"""Create a system backup"""
# This would trigger the backup script
logger.info("Backup requested via dashboard")
return {"message": "Backup initiated", "status": "in_progress"}
@dashboard_router.get("/logs")
async def get_system_logs(limit: int = 100):
"""Get system logs"""
try:
# This would read from the application logs
# For now, return mock logs
logs = [
{"timestamp": "2024-01-01T10:00:00", "level": "INFO", "message": "System started"},
{"timestamp": "2024-01-01T10:01:00", "level": "INFO", "message": "Database connected"},
{"timestamp": "2024-01-01T10:02:00", "level": "INFO", "message": "OPC UA server started"}
]
return {"logs": logs[:limit]}
except Exception as e:
logger.error(f"Error getting logs: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to retrieve logs")
# Comprehensive Configuration Endpoints
@dashboard_router.post("/configure/protocol/opcua")
async def configure_opcua_protocol(config: OPCUAConfig):
"""Configure OPC UA protocol"""
try:
success = configuration_manager.configure_protocol(config)
if success:
return {"success": True, "message": "OPC UA protocol configured successfully"}
else:
raise HTTPException(status_code=400, detail="Failed to configure OPC UA protocol")
except Exception as e:
logger.error(f"Error configuring OPC UA protocol: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to configure OPC UA protocol: {str(e)}")
@dashboard_router.post("/configure/protocol/modbus-tcp")
async def configure_modbus_tcp_protocol(config: ModbusTCPConfig):
"""Configure Modbus TCP protocol"""
try:
success = configuration_manager.configure_protocol(config)
if success:
return {"success": True, "message": "Modbus TCP protocol configured successfully"}
else:
raise HTTPException(status_code=400, detail="Failed to configure Modbus TCP protocol")
except Exception as e:
logger.error(f"Error configuring Modbus TCP protocol: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to configure Modbus TCP protocol: {str(e)}")
@dashboard_router.post("/configure/station")
async def configure_pump_station(station: PumpStationConfig):
"""Configure a pump station"""
try:
success = configuration_manager.add_pump_station(station)
if success:
return {"success": True, "message": f"Pump station {station.name} configured successfully"}
else:
raise HTTPException(status_code=400, detail="Failed to configure pump station")
except Exception as e:
logger.error(f"Error configuring pump station: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to configure pump station: {str(e)}")
@dashboard_router.post("/configure/pump")
async def configure_pump(pump: PumpConfig):
"""Configure a pump"""
try:
success = configuration_manager.add_pump(pump)
if success:
return {"success": True, "message": f"Pump {pump.name} configured successfully"}
else:
raise HTTPException(status_code=400, detail="Failed to configure pump")
except Exception as e:
logger.error(f"Error configuring pump: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to configure pump: {str(e)}")
@dashboard_router.post("/configure/safety-limits")
async def configure_safety_limits(limits: SafetyLimitsConfig):
"""Configure safety limits for a pump"""
try:
success = configuration_manager.set_safety_limits(limits)
if success:
return {"success": True, "message": f"Safety limits configured for pump {limits.pump_id}"}
else:
raise HTTPException(status_code=400, detail="Failed to configure safety limits")
except Exception as e:
logger.error(f"Error configuring safety limits: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to configure safety limits: {str(e)}")
@dashboard_router.post("/configure/data-mapping")
async def configure_data_mapping(mapping: DataPointMapping):
"""Configure data point mapping"""
try:
success = configuration_manager.map_data_point(mapping)
if success:
return {"success": True, "message": "Data point mapping configured successfully"}
else:
raise HTTPException(status_code=400, detail="Failed to configure data mapping")
except Exception as e:
logger.error(f"Error configuring data mapping: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to configure data mapping: {str(e)}")
@dashboard_router.post("/discover-hardware")
async def discover_hardware():
"""Auto-discover connected hardware"""
try:
result = configuration_manager.auto_discover_hardware()
return {
"success": result.success,
"discovered_stations": [station.dict() for station in result.discovered_stations],
"discovered_pumps": [pump.dict() for pump in result.discovered_pumps],
"errors": result.errors,
"warnings": result.warnings
}
except Exception as e:
logger.error(f"Error during hardware discovery: {str(e)}")
raise HTTPException(status_code=500, detail=f"Hardware discovery failed: {str(e)}")
@dashboard_router.get("/validate-configuration")
async def validate_current_configuration():
"""Validate current configuration"""
try:
validation_result = configuration_manager.validate_configuration()
return validation_result
except Exception as e:
logger.error(f"Error validating configuration: {str(e)}")
raise HTTPException(status_code=500, detail=f"Configuration validation failed: {str(e)}")
@dashboard_router.get("/export-configuration")
async def export_configuration():
"""Export complete configuration"""
try:
config_data = configuration_manager.export_configuration()
return {
"success": True,
"configuration": config_data,
"message": "Configuration exported successfully"
}
except Exception as e:
logger.error(f"Error exporting configuration: {str(e)}")
raise HTTPException(status_code=500, detail=f"Configuration export failed: {str(e)}")
@dashboard_router.post("/import-configuration")
async def import_configuration(config_data: dict):
"""Import configuration from backup"""
try:
success = configuration_manager.import_configuration(config_data)
if success:
return {"success": True, "message": "Configuration imported successfully"}
else:
raise HTTPException(status_code=400, detail="Failed to import configuration")
except Exception as e:
logger.error(f"Error importing configuration: {str(e)}")
raise HTTPException(status_code=500, detail=f"Configuration import failed: {str(e)}")
def validate_configuration(config: SystemConfig) -> ValidationResult:
"""Validate configuration before applying"""
errors = []
warnings = []
# Database validation
if not config.database.db_host:
errors.append("Database host is required")
if not config.database.db_name:
errors.append("Database name is required")
if not config.database.db_user:
errors.append("Database user is required")
# Port validation
if not (1 <= config.database.db_port <= 65535):
errors.append("Database port must be between 1 and 65535")
if not (1 <= config.opcua.port <= 65535):
errors.append("OPC UA port must be between 1 and 65535")
if not (1 <= config.modbus.port <= 65535):
errors.append("Modbus port must be between 1 and 65535")
if not (1 <= config.rest_api.port <= 65535):
errors.append("REST API port must be between 1 and 65535")
if not (1 <= config.monitoring.health_monitor_port <= 65535):
errors.append("Health monitor port must be between 1 and 65535")
# Security warnings
if config.security.jwt_secret_key == "your-secret-key-change-in-production":
warnings.append("Default JWT secret key detected - please change for production")
if config.security.api_key == "your-api-key-here":
warnings.append("Default API key detected - please change for production")
return ValidationResult(
valid=len(errors) == 0,
errors=errors,
warnings=warnings
)
def save_configuration(config: SystemConfig):
"""Save configuration to settings file"""
try:
# This would update the settings file
# For now, just log the configuration
logger.info(f"Configuration update received: {config.json(indent=2)}")
# In a real implementation, this would:
# 1. Update the settings file
# 2. Restart affected services
# 3. Verify the new configuration
except Exception as e:
logger.error(f"Error saving configuration: {str(e)}")
# SCADA Configuration endpoints
@dashboard_router.get("/scada-status")
async def get_scada_status():
"""Get SCADA system status"""
try:
# Mock data for demonstration
return {
"modbus_enabled": True,
"modbus_port": 502,
"opcua_enabled": True,
"opcua_port": 4840,
"device_connections": 3,
"data_acquisition": True,
"last_update": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
except Exception as e:
logger.error(f"Error getting SCADA status: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get SCADA status: {str(e)}")
@dashboard_router.get("/scada-config")
async def get_scada_config():
"""Get current SCADA configuration"""
try:
# Get actual configuration from settings
settings = Settings()
# Build device mapping from default stations and pumps
device_mapping_lines = []
stations = ["STATION_001", "STATION_002"]
pumps_by_station = {
"STATION_001": ["PUMP_001", "PUMP_002"],
"STATION_002": ["PUMP_003"]
}
for station_id in stations:
pumps = pumps_by_station.get(station_id, [])
for pump_id in pumps:
device_mapping_lines.append(f"{station_id},{pump_id},OPCUA,Pump_{pump_id}")
device_mapping_lines.append(f"{station_id},{pump_id},Modbus,Pump_{pump_id}")
device_mapping = "\n".join(device_mapping_lines)
return {
"modbus": {
"enabled": True,
"port": settings.modbus_port,
"slave_id": settings.modbus_unit_id,
"baud_rate": "115200"
},
"opcua": {
"enabled": True,
"port": settings.opcua_port,
"security_mode": "SignAndEncrypt"
},
"device_mapping": device_mapping
}
except Exception as e:
logger.error(f"Error getting SCADA configuration: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get SCADA configuration: {str(e)}")
@dashboard_router.post("/scada-config")
async def save_scada_config(config: dict):
"""Save SCADA configuration"""
try:
# In a real implementation, this would save to configuration
logger.info(f"SCADA configuration saved: {config}")
return {"success": True, "message": "SCADA configuration saved successfully"}
except Exception as e:
logger.error(f"Error saving SCADA configuration: {str(e)}")
return {"success": False, "error": str(e)}
@dashboard_router.get("/test-scada")
async def test_scada_connection():
"""Test SCADA connection"""
try:
import socket
# Test OPC UA connection
settings = Settings()
opcua_success = False
modbus_success = False
# Test OPC UA port
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex(('localhost', settings.opcua_port))
sock.close()
opcua_success = result == 0
except:
opcua_success = False
# Test Modbus port
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex(('localhost', settings.modbus_port))
sock.close()
modbus_success = result == 0
except:
modbus_success = False
if opcua_success and modbus_success:
return {"success": True, "message": "SCADA connection test successful - Both OPC UA and Modbus servers are running"}
elif opcua_success:
return {"success": True, "message": "OPC UA server is running, but Modbus server is not accessible"}
elif modbus_success:
return {"success": True, "message": "Modbus server is running, but OPC UA server is not accessible"}
else:
return {"success": False, "error": "Neither OPC UA nor Modbus servers are accessible"}
except Exception as e:
logger.error(f"Error testing SCADA connection: {str(e)}")
return {"success": False, "error": str(e)}
async def _generate_mock_signals(stations: Dict, pumps_by_station: Dict) -> List[Dict[str, Any]]:
"""Generate mock signal data as fallback when protocol servers are not available."""
import random
signals = []
for station_id, station in stations.items():
pumps = pumps_by_station.get(station_id, [])
for pump in pumps:
pump_id = pump['pump_id']
# OPC UA signals for this pump
signals.extend([
{
"name": f"Station_{station_id}_Pump_{pump_id}_Setpoint",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.Setpoint_Hz",
"data_type": "Float",
"current_value": f"{round(random.uniform(0.0, 50.0), 1)} Hz",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_ActualSpeed",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.ActualSpeed_Hz",
"data_type": "Float",
"current_value": f"{round(random.uniform(0.0, 50.0), 1)} Hz",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_Power",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.Power_kW",
"data_type": "Float",
"current_value": f"{round(random.uniform(0.0, 75.0), 1)} kW",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_FlowRate",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.FlowRate_m3h",
"data_type": "Float",
"current_value": f"{round(random.uniform(0.0, 500.0), 1)} m³/h",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_SafetyStatus",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.SafetyStatus",
"data_type": "String",
"current_value": "normal",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
])
# Modbus signals for this pump
# Use consistent register addresses for each pump type
pump_num = int(pump_id.split('_')[1]) # Extract pump number from PUMP_001, PUMP_002, etc.
base_register = 40000 + (pump_num * 10) # Each pump gets 10 registers starting at 40001, 40011, etc.
signals.extend([
{
"name": f"Station_{station_id}_Pump_{pump_id}_Setpoint",
"protocol": "modbus",
"address": f"{base_register + 1}",
"data_type": "Integer",
"current_value": f"{random.randint(0, 500)} Hz (x10)",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_ActualSpeed",
"protocol": "modbus",
"address": f"{base_register + 2}",
"data_type": "Integer",
"current_value": f"{random.randint(0, 500)} Hz (x10)",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_Power",
"protocol": "modbus",
"address": f"{base_register + 3}",
"data_type": "Integer",
"current_value": f"{random.randint(0, 750)} kW (x10)",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_Temperature",
"protocol": "modbus",
"address": f"{base_register + 4}",
"data_type": "Integer",
"current_value": f"{random.randint(20, 35)} °C",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
])
return signals
def _create_fallback_signals(station_id: str, pump_id: str) -> List[Dict[str, Any]]:
"""Create fallback signals when protocol servers are unavailable"""
import random
from datetime import datetime
# Generate realistic mock data
base_setpoint = random.randint(300, 450) # 30-45 Hz
actual_speed = base_setpoint + random.randint(-20, 20)
power = int(actual_speed * 2.5) # Rough power calculation
flow_rate = int(actual_speed * 10) # Rough flow calculation
temperature = random.randint(20, 35) # Normal operating temperature
return [
{
"name": f"Station_{station_id}_Pump_{pump_id}_Setpoint",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.Setpoint_Hz",
"data_type": "Float",
"current_value": f"{base_setpoint / 10:.1f} Hz",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_ActualSpeed",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.ActualSpeed_Hz",
"data_type": "Float",
"current_value": f"{actual_speed / 10:.1f} Hz",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_Power",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.Power_kW",
"data_type": "Float",
"current_value": f"{power / 10:.1f} kW",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_FlowRate",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.FlowRate_m3h",
"data_type": "Float",
"current_value": f"{flow_rate:.1f} m³/h",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_SafetyStatus",
"protocol": "opcua",
"address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.SafetyStatus",
"data_type": "String",
"current_value": "normal",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_Setpoint",
"protocol": "modbus",
"address": f"{40000 + int(pump_id[-1]) * 10 + 1}",
"data_type": "Integer",
"current_value": f"{base_setpoint} Hz (x10)",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_ActualSpeed",
"protocol": "modbus",
"address": f"{40000 + int(pump_id[-1]) * 10 + 2}",
"data_type": "Integer",
"current_value": f"{actual_speed} Hz (x10)",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_Power",
"protocol": "modbus",
"address": f"{40000 + int(pump_id[-1]) * 10 + 3}",
"data_type": "Integer",
"current_value": f"{power} kW (x10)",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": f"Station_{station_id}_Pump_{pump_id}_Temperature",
"protocol": "modbus",
"address": f"{40000 + int(pump_id[-1]) * 10 + 4}",
"data_type": "Integer",
"current_value": f"{temperature} °C",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
]
# Signal Overview endpoints
@dashboard_router.get("/signals")
async def get_signals():
"""Get overview of all active signals across protocols"""
# Use default stations and pumps since we don't have db access in this context
stations = {
"STATION_001": {"name": "Main Pump Station", "location": "Downtown"},
"STATION_002": {"name": "Secondary Pump Station", "location": "Industrial Area"}
}
pumps_by_station = {
"STATION_001": [
{"pump_id": "PUMP_001", "name": "Primary Pump"},
{"pump_id": "PUMP_002", "name": "Backup Pump"}
],
"STATION_002": [
{"pump_id": "PUMP_003", "name": "Industrial Pump"}
]
}
signals = []
# Always use fallback signals for now to avoid protocol client issues
logger.info("using_fallback_signals_for_testing")
for station_id, station in stations.items():
pumps = pumps_by_station.get(station_id, [])
for pump in pumps:
signals.extend(_create_fallback_signals(station_id, pump['pump_id']))
# Add system status signals
signals.extend([
{
"name": "System_Status",
"protocol": "rest",
"address": "/api/v1/dashboard/status",
"data_type": "String",
"current_value": "Running",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": "Database_Connection",
"protocol": "rest",
"address": "/api/v1/dashboard/status",
"data_type": "Boolean",
"current_value": "Connected",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
{
"name": "Health_Status",
"protocol": "rest",
"address": "/api/v1/dashboard/health",
"data_type": "String",
"current_value": "Healthy",
"quality": "Good",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
])
# Calculate protocol statistics
protocol_counts = {}
for signal in signals:
protocol = signal["protocol"]
if protocol not in protocol_counts:
protocol_counts[protocol] = 0
protocol_counts[protocol] += 1
protocol_stats = {}
for protocol, count in protocol_counts.items():
protocol_stats[protocol] = {
"active_signals": count,
"total_signals": count,
"error_rate": "0%"
}
return {
"signals": signals,
"protocol_stats": protocol_stats,
"total_signals": len(signals),
"last_updated": datetime.now().isoformat()
}
@dashboard_router.get("/signals/export")
async def export_signals():
"""Export signals to CSV format"""
try:
# Get signals data
signals_data = await get_signals()
signals = signals_data["signals"]
# Create CSV content
csv_content = "Signal Name,Protocol,Address,Data Type,Current Value,Quality,Timestamp\n"
for signal in signals:
csv_content += f'{signal["name"]},{signal["protocol"]},{signal["address"]},{signal["data_type"]},{signal["current_value"]},{signal["quality"]},{signal["timestamp"]}\n'
# Return as downloadable file
from fastapi.responses import Response
return Response(
content=csv_content,
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=calejo-signals.csv"}
)
except Exception as e:
logger.error(f"Error exporting signals: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to export signals: {str(e)}")