344 lines
13 KiB
Python
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() |