CalejoControl/src/dashboard/configuration_manager.py

344 lines
13 KiB
Python

"""
Dashboard Configuration Manager
Provides comprehensive SCADA and hardware configuration through the dashboard
"""
import json
import logging
from typing import Dict, List, Optional, Any
from pydantic import BaseModel, validator
from enum import Enum
logger = logging.getLogger(__name__)
class ProtocolType(str, Enum):
OPC_UA = "opcua"
MODBUS_TCP = "modbus_tcp"
MODBUS_RTU = "modbus_rtu"
REST_API = "rest_api"
class SCADAProtocolConfig(BaseModel):
"""Base SCADA protocol configuration"""
protocol_type: ProtocolType
enabled: bool = True
name: str
description: str = ""
class OPCUAConfig(SCADAProtocolConfig):
"""OPC UA protocol configuration"""
protocol_type: ProtocolType = ProtocolType.OPC_UA
endpoint: str = "opc.tcp://0.0.0.0:4840"
security_policy: str = "Basic256Sha256"
certificate_file: str = "/app/certs/server.pem"
private_key_file: str = "/app/certs/server.key"
@validator('endpoint')
def validate_endpoint(cls, v):
if not v.startswith("opc.tcp://"):
raise ValueError("OPC UA endpoint must start with 'opc.tcp://'")
return v
class ModbusTCPConfig(SCADAProtocolConfig):
"""Modbus TCP protocol configuration"""
protocol_type: ProtocolType = ProtocolType.MODBUS_TCP
host: str = "0.0.0.0"
port: int = 502
unit_id: int = 1
timeout: float = 5.0
@validator('port')
def validate_port(cls, v):
if not 1 <= v <= 65535:
raise ValueError("Port must be between 1 and 65535")
return v
class PumpStationConfig(BaseModel):
"""Pump station configuration"""
station_id: str
name: str
location: str = ""
description: str = ""
max_pumps: int = 4
power_capacity: float = 150.0
flow_capacity: float = 500.0
@validator('station_id')
def validate_station_id(cls, v):
if not v.replace('_', '').isalnum():
raise ValueError("Station ID must be alphanumeric with underscores")
return v
class PumpConfig(BaseModel):
"""Individual pump configuration"""
pump_id: str
station_id: str
name: str
type: str = "centrifugal" # centrifugal, submersible, etc.
power_rating: float # kW
max_speed: float # Hz
min_speed: float # Hz
vfd_model: str = ""
manufacturer: str = ""
serial_number: str = ""
@validator('pump_id')
def validate_pump_id(cls, v):
if not v.replace('_', '').isalnum():
raise ValueError("Pump ID must be alphanumeric with underscores")
return v
class SafetyLimitsConfig(BaseModel):
"""Safety limits configuration"""
station_id: str
pump_id: str
hard_min_speed_hz: float = 20.0
hard_max_speed_hz: float = 50.0
hard_min_level_m: Optional[float] = None
hard_max_level_m: Optional[float] = None
hard_max_power_kw: Optional[float] = None
max_speed_change_hz_per_min: float = 30.0
@validator('hard_max_speed_hz')
def validate_speed_limits(cls, v, values):
if 'hard_min_speed_hz' in values and v <= values['hard_min_speed_hz']:
raise ValueError("Maximum speed must be greater than minimum speed")
return v
class DataPointMapping(BaseModel):
"""Data point mapping between protocol and internal representation"""
protocol_type: ProtocolType
station_id: str
pump_id: str
data_type: str # setpoint, actual_speed, status, etc.
protocol_address: str # OPC UA node, Modbus register, etc.
data_type_specific: Dict[str, Any] = {}
class HardwareDiscoveryResult(BaseModel):
"""Result from hardware auto-discovery"""
success: bool
discovered_stations: List[PumpStationConfig] = []
discovered_pumps: List[PumpConfig] = []
errors: List[str] = []
warnings: List[str] = []
class ConfigurationManager:
"""Manages comprehensive system configuration through dashboard"""
def __init__(self):
self.protocol_configs: Dict[ProtocolType, SCADAProtocolConfig] = {}
self.stations: Dict[str, PumpStationConfig] = {}
self.pumps: Dict[str, PumpConfig] = {}
self.safety_limits: Dict[str, SafetyLimitsConfig] = {}
self.data_mappings: List[DataPointMapping] = []
def configure_protocol(self, config: SCADAProtocolConfig) -> bool:
"""Configure a SCADA protocol"""
try:
self.protocol_configs[config.protocol_type] = config
logger.info(f"Configured {config.protocol_type.value} protocol: {config.name}")
return True
except Exception as e:
logger.error(f"Failed to configure protocol {config.protocol_type}: {str(e)}")
return False
def add_pump_station(self, station: PumpStationConfig) -> bool:
"""Add a pump station configuration"""
try:
self.stations[station.station_id] = station
logger.info(f"Added pump station: {station.name} ({station.station_id})")
return True
except Exception as e:
logger.error(f"Failed to add pump station {station.station_id}: {str(e)}")
return False
def add_pump(self, pump: PumpConfig) -> bool:
"""Add a pump configuration"""
try:
# Verify station exists
if pump.station_id not in self.stations:
raise ValueError(f"Station {pump.station_id} does not exist")
self.pumps[pump.pump_id] = pump
logger.info(f"Added pump: {pump.name} ({pump.pump_id}) to station {pump.station_id}")
return True
except Exception as e:
logger.error(f"Failed to add pump {pump.pump_id}: {str(e)}")
return False
def set_safety_limits(self, limits: SafetyLimitsConfig) -> bool:
"""Set safety limits for a pump"""
try:
# Verify pump exists
if limits.pump_id not in self.pumps:
raise ValueError(f"Pump {limits.pump_id} does not exist")
key = f"{limits.station_id}_{limits.pump_id}"
self.safety_limits[key] = limits
logger.info(f"Set safety limits for pump {limits.pump_id}")
return True
except Exception as e:
logger.error(f"Failed to set safety limits for {limits.pump_id}: {str(e)}")
return False
def map_data_point(self, mapping: DataPointMapping) -> bool:
"""Map a data point between protocol and internal representation"""
try:
# Verify protocol is configured
if mapping.protocol_type not in self.protocol_configs:
raise ValueError(f"Protocol {mapping.protocol_type} is not configured")
# Verify pump exists
if mapping.pump_id not in self.pumps:
raise ValueError(f"Pump {mapping.pump_id} does not exist")
self.data_mappings.append(mapping)
logger.info(f"Mapped {mapping.data_type} for pump {mapping.pump_id} to {mapping.protocol_address}")
return True
except Exception as e:
logger.error(f"Failed to map data point for {mapping.pump_id}: {str(e)}")
return False
def auto_discover_hardware(self) -> HardwareDiscoveryResult:
"""Auto-discover connected hardware and SCADA systems"""
result = HardwareDiscoveryResult(success=True)
try:
# This would integrate with actual hardware discovery
# For now, provide mock discovery for demonstration
# Mock OPC UA discovery
if ProtocolType.OPC_UA in self.protocol_configs:
logger.info("Performing OPC UA hardware discovery...")
# Simulate discovering a station via OPC UA
mock_station = PumpStationConfig(
station_id="discovered_station_001",
name="Discovered Pump Station",
location="Building A",
max_pumps=2,
power_capacity=100.0
)
result.discovered_stations.append(mock_station)
# Simulate discovering pumps
mock_pump = PumpConfig(
pump_id="discovered_pump_001",
station_id="discovered_station_001",
name="Discovered Primary Pump",
type="centrifugal",
power_rating=55.0,
max_speed=50.0,
min_speed=20.0
)
result.discovered_pumps.append(mock_pump)
# Mock Modbus discovery
if ProtocolType.MODBUS_TCP in self.protocol_configs:
logger.info("Performing Modbus TCP hardware discovery...")
result.warnings.append("Modbus discovery requires manual configuration")
logger.info(f"Hardware discovery completed: {len(result.discovered_stations)} stations, {len(result.discovered_pumps)} pumps found")
except Exception as e:
result.success = False
result.errors.append(f"Hardware discovery failed: {str(e)}")
logger.error(f"Hardware discovery failed: {str(e)}")
return result
def validate_configuration(self) -> Dict[str, Any]:
"""Validate the complete configuration"""
validation_result = {
"valid": True,
"errors": [],
"warnings": [],
"summary": {}
}
# Check protocol configurations
if not self.protocol_configs:
validation_result["warnings"].append("No SCADA protocols configured")
# Check stations and pumps
if not self.stations:
validation_result["warnings"].append("No pump stations configured")
# Check data mappings
if not self.data_mappings:
validation_result["warnings"].append("No data point mappings configured")
# Check safety limits
pumps_without_limits = set(self.pumps.keys()) - set(limit.pump_id for limit in self.safety_limits.values())
if pumps_without_limits:
validation_result["warnings"].append(f"Pumps without safety limits: {', '.join(pumps_without_limits)}")
# Create summary
validation_result["summary"] = {
"protocols_configured": len(self.protocol_configs),
"stations_configured": len(self.stations),
"pumps_configured": len(self.pumps),
"safety_limits_set": len(self.safety_limits),
"data_mappings": len(self.data_mappings)
}
return validation_result
def export_configuration(self) -> Dict[str, Any]:
"""Export complete configuration for backup"""
return {
"protocols": {pt.value: config.dict() for pt, config in self.protocol_configs.items()},
"stations": {sid: station.dict() for sid, station in self.stations.items()},
"pumps": {pid: pump.dict() for pid, pump in self.pumps.items()},
"safety_limits": {key: limits.dict() for key, limits in self.safety_limits.items()},
"data_mappings": [mapping.dict() for mapping in self.data_mappings]
}
def import_configuration(self, config_data: Dict[str, Any]) -> bool:
"""Import configuration from backup"""
try:
# Clear existing configuration
self.protocol_configs.clear()
self.stations.clear()
self.pumps.clear()
self.safety_limits.clear()
self.data_mappings.clear()
# Import protocols
for pt_str, config_dict in config_data.get("protocols", {}).items():
protocol_type = ProtocolType(pt_str)
if protocol_type == ProtocolType.OPC_UA:
config = OPCUAConfig(**config_dict)
elif protocol_type == ProtocolType.MODBUS_TCP:
config = ModbusTCPConfig(**config_dict)
else:
config = SCADAProtocolConfig(**config_dict)
self.protocol_configs[protocol_type] = config
# Import stations
for sid, station_dict in config_data.get("stations", {}).items():
station = PumpStationConfig(**station_dict)
self.stations[sid] = station
# Import pumps
for pid, pump_dict in config_data.get("pumps", {}).items():
pump = PumpConfig(**pump_dict)
self.pumps[pid] = pump
# Import safety limits
for key, limits_dict in config_data.get("safety_limits", {}).items():
limits = SafetyLimitsConfig(**limits_dict)
self.safety_limits[key] = limits
# Import data mappings
for mapping_dict in config_data.get("data_mappings", []):
mapping = DataPointMapping(**mapping_dict)
self.data_mappings.append(mapping)
logger.info("Configuration imported successfully")
return True
except Exception as e:
logger.error(f"Failed to import configuration: {str(e)}")
return False
# Global configuration manager instance
configuration_manager = ConfigurationManager()