2025-10-30 07:22:00 +00:00
|
|
|
"""
|
|
|
|
|
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
|
2025-11-01 10:28:25 +00:00
|
|
|
from .configuration_manager import (
|
|
|
|
|
configuration_manager, OPCUAConfig, ModbusTCPConfig, PumpStationConfig,
|
|
|
|
|
PumpConfig, SafetyLimitsConfig, DataPointMapping, ProtocolType
|
|
|
|
|
)
|
2025-11-01 12:06:23 +00:00
|
|
|
from datetime import datetime
|
2025-10-30 07:22:00 +00:00
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
2025-11-01 10:28:25 +00:00
|
|
|
# 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)}")
|
|
|
|
|
|
2025-10-30 07:22:00 +00:00
|
|
|
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:
|
2025-11-01 12:06:23 +00:00
|
|
|
logger.error(f"Error saving configuration: {str(e)}")
|
|
|
|
|
|
|
|
|
|
# Signal Overview endpoints
|
|
|
|
|
@dashboard_router.get("/signals")
|
|
|
|
|
async def get_signals():
|
|
|
|
|
"""Get overview of all active signals across protocols"""
|
|
|
|
|
try:
|
|
|
|
|
# Mock data for demonstration - in real implementation, this would query active protocols
|
|
|
|
|
signals = [
|
|
|
|
|
{
|
|
|
|
|
"name": "Temperature_Sensor_1",
|
|
|
|
|
"protocol": "modbus",
|
|
|
|
|
"address": "40001",
|
|
|
|
|
"data_type": "Float32",
|
|
|
|
|
"current_value": "23.5°C",
|
|
|
|
|
"quality": "Good",
|
|
|
|
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "Pressure_Sensor_1",
|
|
|
|
|
"protocol": "modbus",
|
|
|
|
|
"address": "40002",
|
|
|
|
|
"data_type": "Float32",
|
|
|
|
|
"current_value": "101.3 kPa",
|
|
|
|
|
"quality": "Good",
|
|
|
|
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "Flow_Rate_1",
|
|
|
|
|
"protocol": "opcua",
|
|
|
|
|
"address": "ns=2;s=FlowRate",
|
|
|
|
|
"data_type": "Double",
|
|
|
|
|
"current_value": "12.5 L/min",
|
|
|
|
|
"quality": "Good",
|
|
|
|
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "Valve_Position_1",
|
|
|
|
|
"protocol": "profinet",
|
|
|
|
|
"address": "DB1.DBX0.0",
|
|
|
|
|
"data_type": "Bool",
|
|
|
|
|
"current_value": "Open",
|
|
|
|
|
"quality": "Good",
|
|
|
|
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "Motor_Speed_1",
|
|
|
|
|
"protocol": "rest",
|
|
|
|
|
"address": "/api/v1/motors/1/speed",
|
|
|
|
|
"data_type": "Int32",
|
|
|
|
|
"current_value": "1450 RPM",
|
|
|
|
|
"quality": "Good",
|
|
|
|
|
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
protocol_stats = {
|
|
|
|
|
"modbus": {
|
|
|
|
|
"active_signals": 2,
|
|
|
|
|
"total_signals": 5,
|
|
|
|
|
"error_rate": "0%"
|
|
|
|
|
},
|
|
|
|
|
"opcua": {
|
|
|
|
|
"active_signals": 1,
|
|
|
|
|
"total_signals": 3,
|
|
|
|
|
"error_rate": "0%"
|
|
|
|
|
},
|
|
|
|
|
"profinet": {
|
|
|
|
|
"active_signals": 1,
|
|
|
|
|
"total_signals": 2,
|
|
|
|
|
"error_rate": "0%"
|
|
|
|
|
},
|
|
|
|
|
"rest": {
|
|
|
|
|
"active_signals": 1,
|
|
|
|
|
"total_signals": 4,
|
|
|
|
|
"error_rate": "0%"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"signals": signals,
|
|
|
|
|
"protocol_stats": protocol_stats,
|
|
|
|
|
"total_signals": len(signals),
|
|
|
|
|
"last_updated": datetime.now().isoformat()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error getting signals: {str(e)}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to get signals: {str(e)}")
|
|
|
|
|
|
|
|
|
|
@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)}")
|