feat: Implement configurable pump control preprocessing logic #5
|
|
@ -0,0 +1,70 @@
|
|||
# Legacy System Removal Summary
|
||||
|
||||
## Overview
|
||||
Successfully removed the legacy station/pump configuration system and fully integrated the tag-based metadata system throughout the Calejo Control application.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Configuration Manager (`src/dashboard/configuration_manager.py`)
|
||||
- **Removed legacy classes**: `PumpStationConfig`, `PumpConfig`, `SafetyLimitsConfig`
|
||||
- **Updated `ProtocolMapping` model**: Added validators to check `station_id`, `equipment_id`, and `data_type_id` against the tag metadata system
|
||||
- **Updated `HardwareDiscoveryResult`**: Changed from legacy class references to generic dictionaries
|
||||
- **Cleaned up configuration methods**: Removed legacy configuration export/import methods
|
||||
|
||||
### 2. API Endpoints (`src/dashboard/api.py`)
|
||||
- **Removed legacy endpoints**: `/configure/station`, `/configure/pump`, `/configure/safety-limits`
|
||||
- **Added tag metadata endpoints**: `/metadata/stations`, `/metadata/equipment`, `/metadata/data-types`
|
||||
- **Updated protocol mapping endpoints**: Now validate against tag metadata system
|
||||
|
||||
### 3. UI Templates (`src/dashboard/templates.py`)
|
||||
- **Replaced text inputs with dropdowns**: For `station_id`, `equipment_id`, and `data_type_id` fields
|
||||
- **Added dynamic loading**: Dropdowns are populated from tag metadata API endpoints
|
||||
- **Updated form validation**: Now validates against available tag metadata
|
||||
|
||||
### 4. JavaScript (`static/protocol_mapping.js`)
|
||||
- **Added tag metadata loading functions**: `loadTagMetadata()`, `populateStationDropdown()`, `populateEquipmentDropdown()`, `populateDataTypeDropdown()`
|
||||
- **Updated form handling**: Now validates against tag metadata before submission
|
||||
- **Enhanced user experience**: Dropdowns provide selection from available tag metadata
|
||||
|
||||
### 5. Security Module (`src/core/security.py`)
|
||||
- **Removed legacy permissions**: `configure_safety_limits` permission removed from ENGINEER and ADMINISTRATOR roles
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Validation System
|
||||
- **Station Validation**: `station_id` must exist in tag metadata stations
|
||||
- **Equipment Validation**: `equipment_id` must exist in tag metadata equipment
|
||||
- **Data Type Validation**: `data_type_id` must exist in tag metadata data types
|
||||
|
||||
### API Integration
|
||||
- **Metadata Endpoints**: Provide real-time access to tag metadata
|
||||
- **Protocol Mapping**: All mappings now reference tag metadata IDs
|
||||
- **Error Handling**: Clear validation errors when tag metadata doesn't exist
|
||||
|
||||
### User Interface
|
||||
- **Dropdown Selection**: Users select from available tag metadata instead of manual entry
|
||||
- **Dynamic Loading**: Dropdowns populated from API endpoints on page load
|
||||
- **Validation Feedback**: Clear error messages when invalid selections are made
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Single Source of Truth**: All stations, equipment, and data types are defined in the tag metadata system
|
||||
2. **Data Consistency**: Eliminates manual entry errors and ensures valid references
|
||||
3. **Improved User Experience**: Dropdown selection is faster and more reliable than manual entry
|
||||
4. **System Integrity**: Validators prevent invalid configurations from being saved
|
||||
5. **Maintainability**: Simplified codebase with unified metadata approach
|
||||
|
||||
## Testing
|
||||
|
||||
All integration tests passed:
|
||||
- ✅ Configuration manager imports without legacy classes
|
||||
- ✅ ProtocolMapping validators check against tag metadata system
|
||||
- ✅ API endpoints use tag metadata system
|
||||
- ✅ UI templates use dropdowns instead of text inputs
|
||||
- ✅ Legacy endpoints and classes completely removed
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Existing protocol mappings will need to be updated to use valid tag metadata IDs
|
||||
- Tag metadata must be populated before creating new protocol mappings
|
||||
- The system now requires all stations, equipment, and data types to be defined in the tag metadata system before use
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
"""
|
||||
Metadata Manager for Calejo Control Adapter
|
||||
|
||||
Provides industry-agnostic metadata management for:
|
||||
- Stations/Assets
|
||||
- Equipment/Devices
|
||||
- Data types and signal mappings
|
||||
- Signal preprocessing rules
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any, Union
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel, validator
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class IndustryType(str, Enum):
|
||||
"""Supported industry types"""
|
||||
WASTEWATER = "wastewater"
|
||||
WATER_TREATMENT = "water_treatment"
|
||||
MANUFACTURING = "manufacturing"
|
||||
ENERGY = "energy"
|
||||
HVAC = "hvac"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
class DataCategory(str, Enum):
|
||||
"""Data categories for different signal types"""
|
||||
CONTROL = "control" # Setpoints, commands
|
||||
MONITORING = "monitoring" # Status, measurements
|
||||
SAFETY = "safety" # Safety limits, emergency stops
|
||||
DIAGNOSTIC = "diagnostic" # Diagnostics, health
|
||||
OPTIMIZATION = "optimization" # Optimization outputs
|
||||
|
||||
|
||||
class SignalTransformation(BaseModel):
|
||||
"""Signal transformation rule for preprocessing"""
|
||||
name: str
|
||||
transformation_type: str # scale, offset, clamp, linear_map, custom
|
||||
parameters: Dict[str, Any]
|
||||
description: str = ""
|
||||
|
||||
@validator('transformation_type')
|
||||
def validate_transformation_type(cls, v):
|
||||
valid_types = ['scale', 'offset', 'clamp', 'linear_map', 'custom']
|
||||
if v not in valid_types:
|
||||
raise ValueError(f"Transformation type must be one of: {valid_types}")
|
||||
return v
|
||||
|
||||
|
||||
class DataTypeMapping(BaseModel):
|
||||
"""Data type mapping configuration"""
|
||||
data_type: str
|
||||
category: DataCategory
|
||||
unit: str
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
default_value: Optional[float] = None
|
||||
transformation_rules: List[SignalTransformation] = []
|
||||
description: str = ""
|
||||
|
||||
|
||||
class AssetMetadata(BaseModel):
|
||||
"""Base asset metadata (station/equipment)"""
|
||||
asset_id: str
|
||||
name: str
|
||||
industry_type: IndustryType
|
||||
location: Optional[str] = None
|
||||
coordinates: Optional[Dict[str, float]] = None
|
||||
metadata: Dict[str, Any] = {}
|
||||
|
||||
@validator('asset_id')
|
||||
def validate_asset_id(cls, v):
|
||||
if not v.replace('_', '').isalnum():
|
||||
raise ValueError("Asset ID must be alphanumeric with underscores")
|
||||
return v
|
||||
|
||||
|
||||
class StationMetadata(AssetMetadata):
|
||||
"""Station/Plant metadata"""
|
||||
station_type: str = "general"
|
||||
capacity: Optional[float] = None
|
||||
equipment_count: int = 0
|
||||
|
||||
|
||||
class EquipmentMetadata(AssetMetadata):
|
||||
"""Equipment/Device metadata"""
|
||||
station_id: str
|
||||
equipment_type: str
|
||||
manufacturer: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
control_type: Optional[str] = None
|
||||
rated_power: Optional[float] = None
|
||||
min_operating_value: Optional[float] = None
|
||||
max_operating_value: Optional[float] = None
|
||||
default_setpoint: Optional[float] = None
|
||||
|
||||
|
||||
class MetadataManager:
|
||||
"""Manages metadata across different industries and data sources"""
|
||||
|
||||
def __init__(self, db_client=None):
|
||||
self.db_client = db_client
|
||||
self.stations: Dict[str, StationMetadata] = {}
|
||||
self.equipment: Dict[str, EquipmentMetadata] = {}
|
||||
self.data_types: Dict[str, DataTypeMapping] = {}
|
||||
self.industry_configs: Dict[IndustryType, Dict[str, Any]] = {}
|
||||
|
||||
# Initialize with default data types
|
||||
self._initialize_default_data_types()
|
||||
|
||||
def _initialize_default_data_types(self):
|
||||
"""Initialize default data types for common industries"""
|
||||
|
||||
# Control data types
|
||||
self.data_types["setpoint"] = DataTypeMapping(
|
||||
data_type="setpoint",
|
||||
category=DataCategory.CONTROL,
|
||||
unit="Hz",
|
||||
min_value=20.0,
|
||||
max_value=50.0,
|
||||
default_value=35.0,
|
||||
description="Frequency setpoint for VFD control"
|
||||
)
|
||||
|
||||
self.data_types["pressure_setpoint"] = DataTypeMapping(
|
||||
data_type="pressure_setpoint",
|
||||
category=DataCategory.CONTROL,
|
||||
unit="bar",
|
||||
min_value=0.0,
|
||||
max_value=10.0,
|
||||
description="Pressure setpoint for pump control"
|
||||
)
|
||||
|
||||
# Monitoring data types
|
||||
self.data_types["actual_speed"] = DataTypeMapping(
|
||||
data_type="actual_speed",
|
||||
category=DataCategory.MONITORING,
|
||||
unit="Hz",
|
||||
description="Actual motor speed"
|
||||
)
|
||||
|
||||
self.data_types["power"] = DataTypeMapping(
|
||||
data_type="power",
|
||||
category=DataCategory.MONITORING,
|
||||
unit="kW",
|
||||
description="Power consumption"
|
||||
)
|
||||
|
||||
self.data_types["flow"] = DataTypeMapping(
|
||||
data_type="flow",
|
||||
category=DataCategory.MONITORING,
|
||||
unit="m³/h",
|
||||
description="Flow rate"
|
||||
)
|
||||
|
||||
self.data_types["level"] = DataTypeMapping(
|
||||
data_type="level",
|
||||
category=DataCategory.MONITORING,
|
||||
unit="m",
|
||||
description="Liquid level"
|
||||
)
|
||||
|
||||
# Safety data types
|
||||
self.data_types["emergency_stop"] = DataTypeMapping(
|
||||
data_type="emergency_stop",
|
||||
category=DataCategory.SAFETY,
|
||||
unit="boolean",
|
||||
description="Emergency stop status"
|
||||
)
|
||||
|
||||
# Optimization data types
|
||||
self.data_types["optimized_setpoint"] = DataTypeMapping(
|
||||
data_type="optimized_setpoint",
|
||||
category=DataCategory.OPTIMIZATION,
|
||||
unit="Hz",
|
||||
min_value=20.0,
|
||||
max_value=50.0,
|
||||
description="Optimized frequency setpoint from AI/ML"
|
||||
)
|
||||
|
||||
def add_station(self, station: StationMetadata) -> bool:
|
||||
"""Add a station to metadata manager"""
|
||||
try:
|
||||
self.stations[station.asset_id] = station
|
||||
logger.info("station_added", station_id=station.asset_id, industry=station.industry_type)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("failed_to_add_station", station_id=station.asset_id, error=str(e))
|
||||
return False
|
||||
|
||||
def add_equipment(self, equipment: EquipmentMetadata) -> bool:
|
||||
"""Add equipment to metadata manager"""
|
||||
try:
|
||||
# Verify station exists
|
||||
if equipment.station_id not in self.stations:
|
||||
logger.warning("unknown_station_for_equipment",
|
||||
equipment_id=equipment.asset_id, station_id=equipment.station_id)
|
||||
|
||||
self.equipment[equipment.asset_id] = equipment
|
||||
|
||||
# Update station equipment count
|
||||
if equipment.station_id in self.stations:
|
||||
self.stations[equipment.station_id].equipment_count += 1
|
||||
|
||||
logger.info("equipment_added",
|
||||
equipment_id=equipment.asset_id,
|
||||
station_id=equipment.station_id,
|
||||
equipment_type=equipment.equipment_type)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("failed_to_add_equipment", equipment_id=equipment.asset_id, error=str(e))
|
||||
return False
|
||||
|
||||
def add_data_type(self, data_type: DataTypeMapping) -> bool:
|
||||
"""Add a custom data type"""
|
||||
try:
|
||||
self.data_types[data_type.data_type] = data_type
|
||||
logger.info("data_type_added", data_type=data_type.data_type, category=data_type.category)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("failed_to_add_data_type", data_type=data_type.data_type, error=str(e))
|
||||
return False
|
||||
|
||||
def get_stations(self, industry_type: Optional[IndustryType] = None) -> List[StationMetadata]:
|
||||
"""Get all stations, optionally filtered by industry"""
|
||||
if industry_type:
|
||||
return [station for station in self.stations.values()
|
||||
if station.industry_type == industry_type]
|
||||
return list(self.stations.values())
|
||||
|
||||
def get_equipment(self, station_id: Optional[str] = None) -> List[EquipmentMetadata]:
|
||||
"""Get all equipment, optionally filtered by station"""
|
||||
if station_id:
|
||||
return [equip for equip in self.equipment.values()
|
||||
if equip.station_id == station_id]
|
||||
return list(self.equipment.values())
|
||||
|
||||
def get_data_types(self, category: Optional[DataCategory] = None) -> List[DataTypeMapping]:
|
||||
"""Get all data types, optionally filtered by category"""
|
||||
if category:
|
||||
return [dt for dt in self.data_types.values() if dt.category == category]
|
||||
return list(self.data_types.values())
|
||||
|
||||
def get_available_data_types_for_equipment(self, equipment_id: str) -> List[DataTypeMapping]:
|
||||
"""Get data types suitable for specific equipment"""
|
||||
equipment = self.equipment.get(equipment_id)
|
||||
if not equipment:
|
||||
return []
|
||||
|
||||
# Filter data types based on equipment type and industry
|
||||
suitable_types = []
|
||||
for data_type in self.data_types.values():
|
||||
# Basic filtering logic - can be extended based on equipment metadata
|
||||
if data_type.category in [DataCategory.CONTROL, DataCategory.MONITORING, DataCategory.OPTIMIZATION]:
|
||||
suitable_types.append(data_type)
|
||||
|
||||
return suitable_types
|
||||
|
||||
def apply_transformation(self, value: float, data_type: str) -> float:
|
||||
"""Apply transformation rules to a value"""
|
||||
if data_type not in self.data_types:
|
||||
return value
|
||||
|
||||
data_type_config = self.data_types[data_type]
|
||||
transformed_value = value
|
||||
|
||||
for transformation in data_type_config.transformation_rules:
|
||||
transformed_value = self._apply_single_transformation(transformed_value, transformation)
|
||||
|
||||
return transformed_value
|
||||
|
||||
def _apply_single_transformation(self, value: float, transformation: SignalTransformation) -> float:
|
||||
"""Apply a single transformation rule"""
|
||||
params = transformation.parameters
|
||||
|
||||
if transformation.transformation_type == "scale":
|
||||
return value * params.get("factor", 1.0)
|
||||
|
||||
elif transformation.transformation_type == "offset":
|
||||
return value + params.get("offset", 0.0)
|
||||
|
||||
elif transformation.transformation_type == "clamp":
|
||||
min_val = params.get("min", float('-inf'))
|
||||
max_val = params.get("max", float('inf'))
|
||||
return max(min_val, min(value, max_val))
|
||||
|
||||
elif transformation.transformation_type == "linear_map":
|
||||
# Map from [input_min, input_max] to [output_min, output_max]
|
||||
input_min = params.get("input_min", 0.0)
|
||||
input_max = params.get("input_max", 1.0)
|
||||
output_min = params.get("output_min", 0.0)
|
||||
output_max = params.get("output_max", 1.0)
|
||||
|
||||
if input_max == input_min:
|
||||
return output_min
|
||||
|
||||
normalized = (value - input_min) / (input_max - input_min)
|
||||
return output_min + normalized * (output_max - output_min)
|
||||
|
||||
# For custom transformations, would need to implement specific logic
|
||||
return value
|
||||
|
||||
def get_metadata_summary(self) -> Dict[str, Any]:
|
||||
"""Get summary of all metadata"""
|
||||
return {
|
||||
"station_count": len(self.stations),
|
||||
"equipment_count": len(self.equipment),
|
||||
"data_type_count": len(self.data_types),
|
||||
"stations_by_industry": {
|
||||
industry.value: len([s for s in self.stations.values() if s.industry_type == industry])
|
||||
for industry in IndustryType
|
||||
},
|
||||
"data_types_by_category": {
|
||||
category.value: len([dt for dt in self.data_types.values() if dt.category == category])
|
||||
for category in DataCategory
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Global metadata manager instance
|
||||
metadata_manager = MetadataManager()
|
||||
|
|
@ -236,7 +236,6 @@ class AuthorizationManager:
|
|||
"emergency_stop",
|
||||
"clear_emergency_stop",
|
||||
"view_alerts",
|
||||
"configure_safety_limits",
|
||||
"manage_pump_configuration",
|
||||
"view_system_metrics"
|
||||
},
|
||||
|
|
@ -247,7 +246,6 @@ class AuthorizationManager:
|
|||
"emergency_stop",
|
||||
"clear_emergency_stop",
|
||||
"view_alerts",
|
||||
"configure_safety_limits",
|
||||
"manage_pump_configuration",
|
||||
"view_system_metrics",
|
||||
"manage_users",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,308 @@
|
|||
"""
|
||||
Tag-Based Metadata Manager
|
||||
|
||||
A flexible, tag-based metadata system that replaces the industry-specific approach.
|
||||
Users can define their own tags and attributes for stations, equipment, and data types.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any, Set
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, asdict
|
||||
import uuid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TagCategory(Enum):
|
||||
"""Core tag categories for consistency"""
|
||||
FUNCTION = "function"
|
||||
SIGNAL_TYPE = "signal_type"
|
||||
EQUIPMENT_TYPE = "equipment_type"
|
||||
LOCATION = "location"
|
||||
STATUS = "status"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tag:
|
||||
"""Individual tag with optional description"""
|
||||
name: str
|
||||
category: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetadataEntity:
|
||||
"""Base class for all metadata entities"""
|
||||
id: str
|
||||
name: str
|
||||
tags: List[str]
|
||||
attributes: Dict[str, Any]
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Station(MetadataEntity):
|
||||
"""Station metadata"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Equipment(MetadataEntity):
|
||||
"""Equipment metadata"""
|
||||
station_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataType(MetadataEntity):
|
||||
"""Data type metadata"""
|
||||
units: Optional[str] = None
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
default_value: Optional[float] = None
|
||||
|
||||
|
||||
class TagMetadataManager:
|
||||
"""
|
||||
Tag-based metadata management system
|
||||
|
||||
Features:
|
||||
- User-defined tags and attributes
|
||||
- System-suggested core tags
|
||||
- Flexible search and filtering
|
||||
- No industry-specific assumptions
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.stations: Dict[str, Station] = {}
|
||||
self.equipment: Dict[str, Equipment] = {}
|
||||
self.data_types: Dict[str, DataType] = {}
|
||||
self.all_tags: Set[str] = set()
|
||||
|
||||
# Core suggested tags (users can ignore these)
|
||||
self._initialize_core_tags()
|
||||
|
||||
logger.info("TagMetadataManager initialized with tag-based approach")
|
||||
|
||||
def _initialize_core_tags(self):
|
||||
"""Initialize core suggested tags for consistency"""
|
||||
core_tags = {
|
||||
# Function tags
|
||||
"control", "monitoring", "safety", "diagnostic", "optimization",
|
||||
|
||||
# Signal type tags
|
||||
"setpoint", "measurement", "status", "alarm", "command", "feedback",
|
||||
|
||||
# Equipment type tags
|
||||
"pump", "valve", "motor", "sensor", "controller", "actuator",
|
||||
|
||||
# Location tags
|
||||
"primary", "secondary", "backup", "emergency", "remote", "local",
|
||||
|
||||
# Status tags
|
||||
"active", "inactive", "maintenance", "fault", "healthy"
|
||||
}
|
||||
|
||||
self.all_tags.update(core_tags)
|
||||
|
||||
def add_station(self,
|
||||
name: str,
|
||||
tags: List[str] = None,
|
||||
attributes: Dict[str, Any] = None,
|
||||
description: str = None,
|
||||
station_id: str = None) -> str:
|
||||
"""Add a new station"""
|
||||
station_id = station_id or f"station_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
station = Station(
|
||||
id=station_id,
|
||||
name=name,
|
||||
tags=tags or [],
|
||||
attributes=attributes or {},
|
||||
description=description
|
||||
)
|
||||
|
||||
self.stations[station_id] = station
|
||||
self.all_tags.update(station.tags)
|
||||
|
||||
logger.info(f"Added station: {station_id} with tags: {station.tags}")
|
||||
return station_id
|
||||
|
||||
def add_equipment(self,
|
||||
name: str,
|
||||
station_id: str,
|
||||
tags: List[str] = None,
|
||||
attributes: Dict[str, Any] = None,
|
||||
description: str = None,
|
||||
equipment_id: str = None) -> str:
|
||||
"""Add new equipment to a station"""
|
||||
if station_id not in self.stations:
|
||||
raise ValueError(f"Station {station_id} does not exist")
|
||||
|
||||
equipment_id = equipment_id or f"equipment_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
equipment = Equipment(
|
||||
id=equipment_id,
|
||||
name=name,
|
||||
station_id=station_id,
|
||||
tags=tags or [],
|
||||
attributes=attributes or {},
|
||||
description=description
|
||||
)
|
||||
|
||||
self.equipment[equipment_id] = equipment
|
||||
self.all_tags.update(equipment.tags)
|
||||
|
||||
logger.info(f"Added equipment: {equipment_id} to station {station_id}")
|
||||
return equipment_id
|
||||
|
||||
def add_data_type(self,
|
||||
name: str,
|
||||
tags: List[str] = None,
|
||||
attributes: Dict[str, Any] = None,
|
||||
description: str = None,
|
||||
units: str = None,
|
||||
min_value: float = None,
|
||||
max_value: float = None,
|
||||
default_value: float = None,
|
||||
data_type_id: str = None) -> str:
|
||||
"""Add a new data type"""
|
||||
data_type_id = data_type_id or f"datatype_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
data_type = DataType(
|
||||
id=data_type_id,
|
||||
name=name,
|
||||
tags=tags or [],
|
||||
attributes=attributes or {},
|
||||
description=description,
|
||||
units=units,
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
default_value=default_value
|
||||
)
|
||||
|
||||
self.data_types[data_type_id] = data_type
|
||||
self.all_tags.update(data_type.tags)
|
||||
|
||||
logger.info(f"Added data type: {data_type_id} with tags: {data_type.tags}")
|
||||
return data_type_id
|
||||
|
||||
def get_stations_by_tags(self, tags: List[str]) -> List[Station]:
|
||||
"""Get stations that have ALL specified tags"""
|
||||
return [
|
||||
station for station in self.stations.values()
|
||||
if all(tag in station.tags for tag in tags)
|
||||
]
|
||||
|
||||
def get_equipment_by_tags(self, tags: List[str], station_id: str = None) -> List[Equipment]:
|
||||
"""Get equipment that has ALL specified tags"""
|
||||
equipment_list = self.equipment.values()
|
||||
|
||||
if station_id:
|
||||
equipment_list = [eq for eq in equipment_list if eq.station_id == station_id]
|
||||
|
||||
return [
|
||||
equipment for equipment in equipment_list
|
||||
if all(tag in equipment.tags for tag in tags)
|
||||
]
|
||||
|
||||
def get_data_types_by_tags(self, tags: List[str]) -> List[DataType]:
|
||||
"""Get data types that have ALL specified tags"""
|
||||
return [
|
||||
data_type for data_type in self.data_types.values()
|
||||
if all(tag in data_type.tags for tag in tags)
|
||||
]
|
||||
|
||||
def search_by_tags(self, tags: List[str]) -> Dict[str, List[Any]]:
|
||||
"""Search across all entities by tags"""
|
||||
return {
|
||||
"stations": self.get_stations_by_tags(tags),
|
||||
"equipment": self.get_equipment_by_tags(tags),
|
||||
"data_types": self.get_data_types_by_tags(tags)
|
||||
}
|
||||
|
||||
def get_suggested_tags(self) -> List[str]:
|
||||
"""Get all available tags (core + user-defined)"""
|
||||
return sorted(list(self.all_tags))
|
||||
|
||||
def get_metadata_summary(self) -> Dict[str, Any]:
|
||||
"""Get summary of all metadata"""
|
||||
return {
|
||||
"stations_count": len(self.stations),
|
||||
"equipment_count": len(self.equipment),
|
||||
"data_types_count": len(self.data_types),
|
||||
"total_tags": len(self.all_tags),
|
||||
"suggested_tags": self.get_suggested_tags(),
|
||||
"stations": [asdict(station) for station in self.stations.values()],
|
||||
"equipment": [asdict(eq) for eq in self.equipment.values()],
|
||||
"data_types": [asdict(dt) for dt in self.data_types.values()]
|
||||
}
|
||||
|
||||
def add_custom_tag(self, tag: str):
|
||||
"""Add a custom tag to the system"""
|
||||
if tag and tag.strip():
|
||||
self.all_tags.add(tag.strip().lower())
|
||||
logger.info(f"Added custom tag: {tag}")
|
||||
|
||||
def remove_tag_from_entity(self, entity_type: str, entity_id: str, tag: str):
|
||||
"""Remove a tag from a specific entity"""
|
||||
entity_map = {
|
||||
"station": self.stations,
|
||||
"equipment": self.equipment,
|
||||
"data_type": self.data_types
|
||||
}
|
||||
|
||||
if entity_type not in entity_map:
|
||||
raise ValueError(f"Invalid entity type: {entity_type}")
|
||||
|
||||
entity = entity_map[entity_type].get(entity_id)
|
||||
if not entity:
|
||||
raise ValueError(f"{entity_type} {entity_id} not found")
|
||||
|
||||
if tag in entity.tags:
|
||||
entity.tags.remove(tag)
|
||||
logger.info(f"Removed tag '{tag}' from {entity_type} {entity_id}")
|
||||
|
||||
def export_metadata(self) -> Dict[str, Any]:
|
||||
"""Export all metadata for backup/transfer"""
|
||||
return {
|
||||
"stations": {id: asdict(station) for id, station in self.stations.items()},
|
||||
"equipment": {id: asdict(eq) for id, eq in self.equipment.items()},
|
||||
"data_types": {id: asdict(dt) for id, dt in self.data_types.items()},
|
||||
"all_tags": list(self.all_tags)
|
||||
}
|
||||
|
||||
def import_metadata(self, data: Dict[str, Any]):
|
||||
"""Import metadata from backup"""
|
||||
try:
|
||||
# Clear existing data
|
||||
self.stations.clear()
|
||||
self.equipment.clear()
|
||||
self.data_types.clear()
|
||||
self.all_tags.clear()
|
||||
|
||||
# Import stations
|
||||
for station_id, station_data in data.get("stations", {}).items():
|
||||
self.stations[station_id] = Station(**station_data)
|
||||
|
||||
# Import equipment
|
||||
for eq_id, eq_data in data.get("equipment", {}).items():
|
||||
self.equipment[eq_id] = Equipment(**eq_data)
|
||||
|
||||
# Import data types
|
||||
for dt_id, dt_data in data.get("data_types", {}).items():
|
||||
self.data_types[dt_id] = DataType(**dt_data)
|
||||
|
||||
# Import tags
|
||||
self.all_tags.update(data.get("all_tags", []))
|
||||
|
||||
logger.info("Successfully imported metadata")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import metadata: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
# Global instance
|
||||
tag_metadata_manager = TagMetadataManager()
|
||||
|
|
@ -12,10 +12,10 @@ from pydantic import BaseModel, ValidationError
|
|||
|
||||
from config.settings import Settings
|
||||
from .configuration_manager import (
|
||||
configuration_manager, OPCUAConfig, ModbusTCPConfig, PumpStationConfig,
|
||||
PumpConfig, SafetyLimitsConfig, DataPointMapping, ProtocolType, ProtocolMapping
|
||||
configuration_manager, OPCUAConfig, ModbusTCPConfig, DataPointMapping, ProtocolType, ProtocolMapping
|
||||
)
|
||||
from src.discovery.protocol_discovery_persistent import persistent_discovery_service, DiscoveryStatus, DiscoveredEndpoint
|
||||
from src.core.tag_metadata_manager import tag_metadata_manager
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -218,44 +218,7 @@ async def configure_modbus_tcp_protocol(config: ModbusTCPConfig):
|
|||
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):
|
||||
|
|
@ -830,13 +793,13 @@ async def export_signals():
|
|||
async def get_protocol_mappings(
|
||||
protocol_type: Optional[str] = None,
|
||||
station_id: Optional[str] = None,
|
||||
pump_id: Optional[str] = None
|
||||
equipment_id: Optional[str] = None
|
||||
):
|
||||
"""Get protocol mappings with optional filtering"""
|
||||
try:
|
||||
# Convert protocol_type string to enum if provided
|
||||
protocol_enum = None
|
||||
if protocol_type:
|
||||
if protocol_type and protocol_type != "all":
|
||||
try:
|
||||
protocol_enum = ProtocolType(protocol_type)
|
||||
except ValueError:
|
||||
|
|
@ -845,7 +808,7 @@ async def get_protocol_mappings(
|
|||
mappings = configuration_manager.get_protocol_mappings(
|
||||
protocol_type=protocol_enum,
|
||||
station_id=station_id,
|
||||
pump_id=pump_id
|
||||
equipment_id=equipment_id
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -873,14 +836,19 @@ async def create_protocol_mapping(mapping_data: dict):
|
|||
# Create ProtocolMapping object
|
||||
import uuid
|
||||
mapping = ProtocolMapping(
|
||||
id=mapping_data.get("id") or f"{mapping_data.get('protocol_type')}_{mapping_data.get('station_id', 'unknown')}_{mapping_data.get('pump_id', 'unknown')}_{uuid.uuid4().hex[:8]}",
|
||||
id=mapping_data.get("id") or f"{mapping_data.get('protocol_type')}_{mapping_data.get('station_id', 'unknown')}_{mapping_data.get('equipment_id', 'unknown')}_{uuid.uuid4().hex[:8]}",
|
||||
protocol_type=protocol_enum,
|
||||
station_id=mapping_data.get("station_id"),
|
||||
pump_id=mapping_data.get("pump_id"),
|
||||
data_type=mapping_data.get("data_type"),
|
||||
equipment_id=mapping_data.get("equipment_id"),
|
||||
data_type_id=mapping_data.get("data_type_id"),
|
||||
protocol_address=mapping_data.get("protocol_address"),
|
||||
db_source=mapping_data.get("db_source"),
|
||||
transformation_rules=mapping_data.get("transformation_rules", []),
|
||||
preprocessing_enabled=mapping_data.get("preprocessing_enabled", False),
|
||||
preprocessing_rules=mapping_data.get("preprocessing_rules", []),
|
||||
min_output_value=mapping_data.get("min_output_value"),
|
||||
max_output_value=mapping_data.get("max_output_value"),
|
||||
default_output_value=mapping_data.get("default_output_value"),
|
||||
modbus_config=mapping_data.get("modbus_config"),
|
||||
opcua_config=mapping_data.get("opcua_config")
|
||||
)
|
||||
|
|
@ -923,8 +891,8 @@ async def update_protocol_mapping(mapping_id: str, mapping_data: dict):
|
|||
id=mapping_id, # Use the ID from URL
|
||||
protocol_type=protocol_enum or ProtocolType(mapping_data.get("protocol_type")),
|
||||
station_id=mapping_data.get("station_id"),
|
||||
pump_id=mapping_data.get("pump_id"),
|
||||
data_type=mapping_data.get("data_type"),
|
||||
equipment_id=mapping_data.get("equipment_id"),
|
||||
data_type_id=mapping_data.get("data_type_id"),
|
||||
protocol_address=mapping_data.get("protocol_address"),
|
||||
db_source=mapping_data.get("db_source"),
|
||||
transformation_rules=mapping_data.get("transformation_rules", []),
|
||||
|
|
@ -971,6 +939,181 @@ async def delete_protocol_mapping(mapping_id: str):
|
|||
|
||||
# Protocol Discovery API Endpoints
|
||||
|
||||
# Tag-Based Metadata API Endpoints
|
||||
|
||||
@dashboard_router.get("/metadata/summary")
|
||||
async def get_metadata_summary():
|
||||
"""Get tag-based metadata summary"""
|
||||
try:
|
||||
summary = tag_metadata_manager.get_metadata_summary()
|
||||
return {
|
||||
"success": True,
|
||||
"summary": summary
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting metadata summary: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get metadata summary: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/stations")
|
||||
async def get_stations(tags: Optional[str] = None):
|
||||
"""Get stations, optionally filtered by tags (comma-separated)"""
|
||||
try:
|
||||
tag_list = tags.split(",") if tags else []
|
||||
stations = tag_metadata_manager.get_stations_by_tags(tag_list)
|
||||
return {
|
||||
"success": True,
|
||||
"stations": stations,
|
||||
"count": len(stations)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting stations: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get stations: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/equipment")
|
||||
async def get_equipment(station_id: Optional[str] = None, tags: Optional[str] = None):
|
||||
"""Get equipment, optionally filtered by station and tags"""
|
||||
try:
|
||||
tag_list = tags.split(",") if tags else []
|
||||
equipment = tag_metadata_manager.get_equipment_by_tags(tag_list, station_id)
|
||||
return {
|
||||
"success": True,
|
||||
"equipment": equipment,
|
||||
"count": len(equipment)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting equipment: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get equipment: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/data-types")
|
||||
async def get_data_types(tags: Optional[str] = None):
|
||||
"""Get data types, optionally filtered by tags"""
|
||||
try:
|
||||
tag_list = tags.split(",") if tags else []
|
||||
data_types = tag_metadata_manager.get_data_types_by_tags(tag_list)
|
||||
return {
|
||||
"success": True,
|
||||
"data_types": data_types,
|
||||
"count": len(data_types)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting data types: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get data types: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/tags")
|
||||
async def get_suggested_tags():
|
||||
"""Get all available tags (core + user-defined)"""
|
||||
try:
|
||||
tags = tag_metadata_manager.get_suggested_tags()
|
||||
return {
|
||||
"success": True,
|
||||
"tags": tags,
|
||||
"count": len(tags)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting tags: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get tags: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/metadata/stations")
|
||||
async def create_station(station_data: dict):
|
||||
"""Create a new station with tags"""
|
||||
try:
|
||||
station_id = tag_metadata_manager.add_station(
|
||||
name=station_data.get("name"),
|
||||
tags=station_data.get("tags", []),
|
||||
attributes=station_data.get("attributes", {}),
|
||||
description=station_data.get("description"),
|
||||
station_id=station_data.get("id")
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"station_id": station_id,
|
||||
"message": "Station created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating station: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create station: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/metadata/equipment")
|
||||
async def create_equipment(equipment_data: dict):
|
||||
"""Create new equipment with tags"""
|
||||
try:
|
||||
equipment_id = tag_metadata_manager.add_equipment(
|
||||
name=equipment_data.get("name"),
|
||||
station_id=equipment_data.get("station_id"),
|
||||
tags=equipment_data.get("tags", []),
|
||||
attributes=equipment_data.get("attributes", {}),
|
||||
description=equipment_data.get("description"),
|
||||
equipment_id=equipment_data.get("id")
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"equipment_id": equipment_id,
|
||||
"message": "Equipment created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating equipment: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create equipment: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/metadata/data-types")
|
||||
async def create_data_type(data_type_data: dict):
|
||||
"""Create new data type with tags"""
|
||||
try:
|
||||
data_type_id = tag_metadata_manager.add_data_type(
|
||||
name=data_type_data.get("name"),
|
||||
tags=data_type_data.get("tags", []),
|
||||
attributes=data_type_data.get("attributes", {}),
|
||||
description=data_type_data.get("description"),
|
||||
units=data_type_data.get("units"),
|
||||
min_value=data_type_data.get("min_value"),
|
||||
max_value=data_type_data.get("max_value"),
|
||||
default_value=data_type_data.get("default_value"),
|
||||
data_type_id=data_type_data.get("id")
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"data_type_id": data_type_id,
|
||||
"message": "Data type created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating data type: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create data type: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/metadata/tags")
|
||||
async def add_custom_tag(tag_data: dict):
|
||||
"""Add a custom tag to the system"""
|
||||
try:
|
||||
tag = tag_data.get("tag")
|
||||
if not tag:
|
||||
raise HTTPException(status_code=400, detail="Tag is required")
|
||||
|
||||
tag_metadata_manager.add_custom_tag(tag)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Tag '{tag}' added successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding tag: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to add tag: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/search")
|
||||
async def search_metadata(tags: str):
|
||||
"""Search across all metadata entities by tags"""
|
||||
try:
|
||||
if not tags:
|
||||
raise HTTPException(status_code=400, detail="Tags parameter is required")
|
||||
|
||||
tag_list = tags.split(",")
|
||||
results = tag_metadata_manager.search_by_tags(tag_list)
|
||||
return {
|
||||
"success": True,
|
||||
"search_tags": tag_list,
|
||||
"results": results
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching metadata: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to search metadata: {str(e)}")
|
||||
|
||||
|
||||
@dashboard_router.get("/discovery/status")
|
||||
async def get_discovery_status():
|
||||
"""Get current discovery service status"""
|
||||
|
|
@ -1097,7 +1240,7 @@ async def get_recent_discoveries():
|
|||
|
||||
|
||||
@dashboard_router.post("/discovery/apply/{scan_id}")
|
||||
async def apply_discovery_results(scan_id: str, station_id: str, pump_id: str, data_type: str, db_source: str):
|
||||
async def apply_discovery_results(scan_id: str, station_id: str, equipment_id: str, data_type_id: str, db_source: str):
|
||||
"""Apply discovered endpoints as protocol mappings"""
|
||||
try:
|
||||
result = persistent_discovery_service.get_scan_result(scan_id)
|
||||
|
|
@ -1114,15 +1257,29 @@ async def apply_discovery_results(scan_id: str, station_id: str, pump_id: str, d
|
|||
for endpoint in result.get("discovered_endpoints", []):
|
||||
try:
|
||||
# Create protocol mapping from discovered endpoint
|
||||
mapping_id = f"{endpoint.get('device_id')}_{data_type}"
|
||||
mapping_id = f"{endpoint.get('device_id')}_{data_type_id}"
|
||||
|
||||
# Convert protocol types to match configuration manager expectations
|
||||
protocol_type = endpoint.get("protocol_type")
|
||||
if protocol_type == "opc_ua":
|
||||
protocol_type = "opcua"
|
||||
|
||||
# Convert addresses based on protocol type
|
||||
protocol_address = endpoint.get("address")
|
||||
if protocol_type == "modbus_tcp":
|
||||
# For Modbus TCP, use a default register address since IP is not valid
|
||||
protocol_address = "40001" # Default holding register
|
||||
elif protocol_type == "opcua":
|
||||
# For OPC UA, construct a proper node ID
|
||||
protocol_address = f"ns=2;s={endpoint.get('device_name', 'Device').replace(' ', '_')}"
|
||||
|
||||
protocol_mapping = ProtocolMapping(
|
||||
id=mapping_id,
|
||||
station_id=station_id,
|
||||
pump_id=pump_id,
|
||||
protocol_type=endpoint.get("protocol_type"),
|
||||
protocol_address=endpoint.get("address"),
|
||||
data_type=data_type,
|
||||
equipment_id=equipment_id,
|
||||
protocol_type=protocol_type,
|
||||
protocol_address=protocol_address,
|
||||
data_type_id=data_type_id,
|
||||
db_source=db_source
|
||||
)
|
||||
|
||||
|
|
@ -1167,8 +1324,8 @@ async def validate_protocol_mapping(mapping_id: str, mapping_data: dict):
|
|||
id=mapping_id,
|
||||
protocol_type=protocol_enum,
|
||||
station_id=mapping_data.get("station_id"),
|
||||
pump_id=mapping_data.get("pump_id"),
|
||||
data_type=mapping_data.get("data_type"),
|
||||
equipment_id=mapping_data.get("equipment_id"),
|
||||
data_type_id=mapping_data.get("data_type_id"),
|
||||
protocol_address=mapping_data.get("protocol_address"),
|
||||
db_source=mapping_data.get("db_source"),
|
||||
transformation_rules=mapping_data.get("transformation_rules", []),
|
||||
|
|
|
|||
|
|
@ -52,57 +52,7 @@ class ModbusTCPConfig(SCADAProtocolConfig):
|
|||
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"""
|
||||
|
|
@ -118,12 +68,19 @@ class ProtocolMapping(BaseModel):
|
|||
id: str
|
||||
protocol_type: ProtocolType
|
||||
station_id: str
|
||||
pump_id: str
|
||||
data_type: str # setpoint, status, power, flow, level, safety, etc.
|
||||
equipment_id: str
|
||||
data_type_id: str
|
||||
protocol_address: str # register address or OPC UA node
|
||||
db_source: str # database table and column
|
||||
transformation_rules: List[Dict[str, Any]] = []
|
||||
|
||||
# Signal preprocessing configuration
|
||||
preprocessing_enabled: bool = False
|
||||
preprocessing_rules: List[Dict[str, Any]] = []
|
||||
min_output_value: Optional[float] = None
|
||||
max_output_value: Optional[float] = None
|
||||
default_output_value: Optional[float] = None
|
||||
|
||||
# Protocol-specific configurations
|
||||
modbus_config: Optional[Dict[str, Any]] = None
|
||||
opcua_config: Optional[Dict[str, Any]] = None
|
||||
|
|
@ -134,6 +91,36 @@ class ProtocolMapping(BaseModel):
|
|||
raise ValueError("Mapping ID must be alphanumeric with underscores")
|
||||
return v
|
||||
|
||||
@validator('station_id')
|
||||
def validate_station_id(cls, v):
|
||||
"""Validate that station exists in tag metadata system"""
|
||||
from src.core.tag_metadata_manager import tag_metadata_manager
|
||||
if v and v not in tag_metadata_manager.stations:
|
||||
raise ValueError(f"Station '{v}' does not exist in tag metadata system")
|
||||
return v
|
||||
|
||||
@validator('equipment_id')
|
||||
def validate_equipment_id(cls, v, values):
|
||||
"""Validate that equipment exists in tag metadata system and belongs to station"""
|
||||
from src.core.tag_metadata_manager import tag_metadata_manager
|
||||
if v and v not in tag_metadata_manager.equipment:
|
||||
raise ValueError(f"Equipment '{v}' does not exist in tag metadata system")
|
||||
|
||||
# Validate equipment belongs to station
|
||||
if 'station_id' in values and values['station_id']:
|
||||
equipment = tag_metadata_manager.equipment.get(v)
|
||||
if equipment and equipment.station_id != values['station_id']:
|
||||
raise ValueError(f"Equipment '{v}' does not belong to station '{values['station_id']}'")
|
||||
return v
|
||||
|
||||
@validator('data_type_id')
|
||||
def validate_data_type_id(cls, v):
|
||||
"""Validate that data type exists in tag metadata system"""
|
||||
from src.core.tag_metadata_manager import tag_metadata_manager
|
||||
if v and v not in tag_metadata_manager.data_types:
|
||||
raise ValueError(f"Data type '{v}' does not exist in tag metadata system")
|
||||
return v
|
||||
|
||||
@validator('protocol_address')
|
||||
def validate_protocol_address(cls, v, values):
|
||||
if 'protocol_type' in values:
|
||||
|
|
@ -158,12 +145,58 @@ class ProtocolMapping(BaseModel):
|
|||
if not v.startswith(('http://', 'https://')):
|
||||
raise ValueError("REST API endpoint must start with 'http://' or 'https://'")
|
||||
return v
|
||||
|
||||
def apply_preprocessing(self, value: float) -> float:
|
||||
"""Apply preprocessing rules to a value"""
|
||||
if not self.preprocessing_enabled:
|
||||
return value
|
||||
|
||||
processed_value = value
|
||||
|
||||
for rule in self.preprocessing_rules:
|
||||
rule_type = rule.get('type')
|
||||
params = rule.get('parameters', {})
|
||||
|
||||
if rule_type == 'scale':
|
||||
processed_value *= params.get('factor', 1.0)
|
||||
elif rule_type == 'offset':
|
||||
processed_value += params.get('offset', 0.0)
|
||||
elif rule_type == 'clamp':
|
||||
min_val = params.get('min', float('-inf'))
|
||||
max_val = params.get('max', float('inf'))
|
||||
processed_value = max(min_val, min(processed_value, max_val))
|
||||
elif rule_type == 'linear_map':
|
||||
# Map from [input_min, input_max] to [output_min, output_max]
|
||||
input_min = params.get('input_min', 0.0)
|
||||
input_max = params.get('input_max', 1.0)
|
||||
output_min = params.get('output_min', 0.0)
|
||||
output_max = params.get('output_max', 1.0)
|
||||
|
||||
if input_max == input_min:
|
||||
processed_value = output_min
|
||||
else:
|
||||
normalized = (processed_value - input_min) / (input_max - input_min)
|
||||
processed_value = output_min + normalized * (output_max - output_min)
|
||||
elif rule_type == 'deadband':
|
||||
# Apply deadband to prevent oscillation
|
||||
center = params.get('center', 0.0)
|
||||
width = params.get('width', 0.0)
|
||||
if abs(processed_value - center) <= width:
|
||||
processed_value = center
|
||||
|
||||
# Apply final output limits
|
||||
if self.min_output_value is not None:
|
||||
processed_value = max(self.min_output_value, processed_value)
|
||||
if self.max_output_value is not None:
|
||||
processed_value = min(self.max_output_value, processed_value)
|
||||
|
||||
return processed_value
|
||||
|
||||
class HardwareDiscoveryResult(BaseModel):
|
||||
"""Result from hardware auto-discovery"""
|
||||
success: bool
|
||||
discovered_stations: List[PumpStationConfig] = []
|
||||
discovered_pumps: List[PumpConfig] = []
|
||||
discovered_stations: List[Dict[str, Any]] = []
|
||||
discovered_pumps: List[Dict[str, Any]] = []
|
||||
errors: List[str] = []
|
||||
warnings: List[str] = []
|
||||
|
||||
|
|
@ -172,9 +205,6 @@ class ConfigurationManager:
|
|||
|
||||
def __init__(self, db_client=None):
|
||||
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] = []
|
||||
self.protocol_mappings: List[ProtocolMapping] = []
|
||||
self.db_client = db_client
|
||||
|
|
@ -187,11 +217,11 @@ class ConfigurationManager:
|
|||
"""Load protocol mappings from database"""
|
||||
try:
|
||||
query = """
|
||||
SELECT mapping_id, station_id, pump_id, protocol_type,
|
||||
protocol_address, data_type, db_source, enabled
|
||||
SELECT mapping_id, station_id, equipment_id, protocol_type,
|
||||
protocol_address, data_type_id, db_source, enabled
|
||||
FROM protocol_mappings
|
||||
WHERE enabled = true
|
||||
ORDER BY station_id, pump_id, protocol_type
|
||||
ORDER BY station_id, equipment_id, protocol_type
|
||||
"""
|
||||
|
||||
results = self.db_client.execute_query(query)
|
||||
|
|
@ -205,10 +235,10 @@ class ConfigurationManager:
|
|||
mapping = ProtocolMapping(
|
||||
id=row['mapping_id'],
|
||||
station_id=row['station_id'],
|
||||
pump_id=row['pump_id'],
|
||||
equipment_id=row['equipment_id'],
|
||||
protocol_type=protocol_type,
|
||||
protocol_address=row['protocol_address'],
|
||||
data_type=row['data_type'],
|
||||
data_type_id=row['data_type_id'],
|
||||
db_source=row['db_source']
|
||||
)
|
||||
self.protocol_mappings.append(mapping)
|
||||
|
|
@ -230,44 +260,7 @@ class ConfigurationManager:
|
|||
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"""
|
||||
|
|
@ -307,14 +300,14 @@ class ConfigurationManager:
|
|||
if self.db_client:
|
||||
query = """
|
||||
INSERT INTO protocol_mappings
|
||||
(mapping_id, station_id, pump_id, protocol_type, protocol_address, data_type, db_source, created_by, enabled)
|
||||
VALUES (:mapping_id, :station_id, :pump_id, :protocol_type, :protocol_address, :data_type, :db_source, :created_by, :enabled)
|
||||
(mapping_id, station_id, equipment_id, protocol_type, protocol_address, data_type_id, db_source, created_by, enabled)
|
||||
VALUES (:mapping_id, :station_id, :equipment_id, :protocol_type, :protocol_address, :data_type_id, :db_source, :created_by, :enabled)
|
||||
ON CONFLICT (mapping_id) DO UPDATE SET
|
||||
station_id = EXCLUDED.station_id,
|
||||
pump_id = EXCLUDED.pump_id,
|
||||
equipment_id = EXCLUDED.equipment_id,
|
||||
protocol_type = EXCLUDED.protocol_type,
|
||||
protocol_address = EXCLUDED.protocol_address,
|
||||
data_type = EXCLUDED.data_type,
|
||||
data_type_id = EXCLUDED.data_type_id,
|
||||
db_source = EXCLUDED.db_source,
|
||||
enabled = EXCLUDED.enabled,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
|
|
@ -322,10 +315,10 @@ class ConfigurationManager:
|
|||
params = {
|
||||
'mapping_id': mapping.id,
|
||||
'station_id': mapping.station_id,
|
||||
'pump_id': mapping.pump_id,
|
||||
'equipment_id': mapping.equipment_id,
|
||||
'protocol_type': mapping.protocol_type.value,
|
||||
'protocol_address': mapping.protocol_address,
|
||||
'data_type': mapping.data_type,
|
||||
'data_type_id': mapping.data_type_id,
|
||||
'db_source': mapping.db_source,
|
||||
'created_by': 'dashboard',
|
||||
'enabled': True
|
||||
|
|
@ -333,7 +326,7 @@ class ConfigurationManager:
|
|||
self.db_client.execute(query, params)
|
||||
|
||||
self.protocol_mappings.append(mapping)
|
||||
logger.info(f"Added protocol mapping {mapping.id}: {mapping.protocol_type} for {mapping.station_id}/{mapping.pump_id}")
|
||||
logger.info(f"Added protocol mapping {mapping.id}: {mapping.protocol_type} for {mapping.station_id}/{mapping.equipment_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add protocol mapping {mapping.id}: {str(e)}")
|
||||
|
|
@ -342,8 +335,8 @@ class ConfigurationManager:
|
|||
def get_protocol_mappings(self,
|
||||
protocol_type: Optional[ProtocolType] = None,
|
||||
station_id: Optional[str] = None,
|
||||
pump_id: Optional[str] = None) -> List[ProtocolMapping]:
|
||||
"""Get mappings filtered by protocol/station/pump"""
|
||||
equipment_id: Optional[str] = None) -> List[ProtocolMapping]:
|
||||
"""Get mappings filtered by protocol/station/equipment"""
|
||||
filtered_mappings = self.protocol_mappings.copy()
|
||||
|
||||
if protocol_type:
|
||||
|
|
@ -352,8 +345,8 @@ class ConfigurationManager:
|
|||
if station_id:
|
||||
filtered_mappings = [m for m in filtered_mappings if m.station_id == station_id]
|
||||
|
||||
if pump_id:
|
||||
filtered_mappings = [m for m in filtered_mappings if m.pump_id == pump_id]
|
||||
if equipment_id:
|
||||
filtered_mappings = [m for m in filtered_mappings if m.equipment_id == equipment_id]
|
||||
|
||||
return filtered_mappings
|
||||
|
||||
|
|
@ -373,10 +366,10 @@ class ConfigurationManager:
|
|||
query = """
|
||||
UPDATE protocol_mappings
|
||||
SET station_id = :station_id,
|
||||
pump_id = :pump_id,
|
||||
equipment_id = :equipment_id,
|
||||
protocol_type = :protocol_type,
|
||||
protocol_address = :protocol_address,
|
||||
data_type = :data_type,
|
||||
data_type_id = :data_type_id,
|
||||
db_source = :db_source,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE mapping_id = :mapping_id
|
||||
|
|
@ -384,10 +377,10 @@ class ConfigurationManager:
|
|||
params = {
|
||||
'mapping_id': mapping_id,
|
||||
'station_id': updated_mapping.station_id,
|
||||
'pump_id': updated_mapping.pump_id,
|
||||
'equipment_id': updated_mapping.equipment_id,
|
||||
'protocol_type': updated_mapping.protocol_type.value,
|
||||
'protocol_address': updated_mapping.protocol_address,
|
||||
'data_type': updated_mapping.data_type,
|
||||
'data_type_id': updated_mapping.data_type_id,
|
||||
'db_source': updated_mapping.db_source
|
||||
}
|
||||
self.db_client.execute(query, params)
|
||||
|
|
@ -445,7 +438,7 @@ class ConfigurationManager:
|
|||
if (existing.id != mapping.id and
|
||||
existing.protocol_type == ProtocolType.MODBUS_TCP and
|
||||
existing.protocol_address == mapping.protocol_address):
|
||||
errors.append(f"Modbus address {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
|
||||
errors.append(f"Modbus address {mapping.protocol_address} already used by {existing.station_id}/{existing.equipment_id}")
|
||||
break
|
||||
|
||||
except ValueError:
|
||||
|
|
@ -461,7 +454,7 @@ class ConfigurationManager:
|
|||
if (existing.id != mapping.id and
|
||||
existing.protocol_type == ProtocolType.OPC_UA and
|
||||
existing.protocol_address == mapping.protocol_address):
|
||||
errors.append(f"OPC UA node {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
|
||||
errors.append(f"OPC UA node {mapping.protocol_address} already used by {existing.station_id}/{existing.equipment_id}")
|
||||
break
|
||||
|
||||
elif mapping.protocol_type == ProtocolType.MODBUS_RTU:
|
||||
|
|
@ -476,7 +469,7 @@ class ConfigurationManager:
|
|||
if (existing.id != mapping.id and
|
||||
existing.protocol_type == ProtocolType.MODBUS_RTU and
|
||||
existing.protocol_address == mapping.protocol_address):
|
||||
errors.append(f"Modbus RTU address {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
|
||||
errors.append(f"Modbus RTU address {mapping.protocol_address} already used by {existing.station_id}/{existing.equipment_id}")
|
||||
break
|
||||
|
||||
except ValueError:
|
||||
|
|
@ -492,7 +485,7 @@ class ConfigurationManager:
|
|||
if (existing.id != mapping.id and
|
||||
existing.protocol_type == ProtocolType.REST_API and
|
||||
existing.protocol_address == mapping.protocol_address):
|
||||
errors.append(f"REST API endpoint {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
|
||||
errors.append(f"REST API endpoint {mapping.protocol_address} already used by {existing.station_id}/{existing.equipment_id}")
|
||||
break
|
||||
|
||||
# Check database source format
|
||||
|
|
@ -517,25 +510,25 @@ class ConfigurationManager:
|
|||
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
|
||||
)
|
||||
mock_station = {
|
||||
"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
|
||||
)
|
||||
mock_pump = {
|
||||
"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
|
||||
|
|
@ -592,9 +585,6 @@ class ConfigurationManager:
|
|||
# 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),
|
||||
"protocol_mappings": len(self.protocol_mappings)
|
||||
}
|
||||
|
|
@ -605,9 +595,6 @@ class ConfigurationManager:
|
|||
"""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],
|
||||
"protocol_mappings": [mapping.dict() for mapping in self.protocol_mappings]
|
||||
}
|
||||
|
|
@ -617,9 +604,6 @@ class ConfigurationManager:
|
|||
try:
|
||||
# Clear existing configuration
|
||||
self.protocol_configs.clear()
|
||||
self.stations.clear()
|
||||
self.pumps.clear()
|
||||
self.safety_limits.clear()
|
||||
self.data_mappings.clear()
|
||||
self.protocol_mappings.clear()
|
||||
|
||||
|
|
@ -634,21 +618,6 @@ class ConfigurationManager:
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -564,7 +564,7 @@ DASHBOARD_HTML = """
|
|||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">ID</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Protocol</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Station</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Pump</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Equipment</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Data Type</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Protocol Address</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Database Source</th>
|
||||
|
|
@ -599,25 +599,25 @@ DASHBOARD_HTML = """
|
|||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="station_id">Station ID:</label>
|
||||
<input type="text" id="station_id" name="station_id" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pump_id">Pump ID:</label>
|
||||
<input type="text" id="pump_id" name="pump_id" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="data_type">Data Type:</label>
|
||||
<select id="data_type" name="data_type" required>
|
||||
<option value="">Select Data Type</option>
|
||||
<option value="setpoint">Setpoint</option>
|
||||
<option value="actual_speed">Actual Speed</option>
|
||||
<option value="status">Status</option>
|
||||
<option value="power">Power</option>
|
||||
<option value="flow">Flow</option>
|
||||
<option value="level">Level</option>
|
||||
<option value="safety">Safety</option>
|
||||
<label for="station_id">Station:</label>
|
||||
<select id="station_id" name="station_id" required>
|
||||
<option value="">Select Station</option>
|
||||
</select>
|
||||
<small style="color: #666;">Stations will be loaded from tag metadata system</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="equipment_id">Equipment:</label>
|
||||
<select id="equipment_id" name="equipment_id" required>
|
||||
<option value="">Select Equipment</option>
|
||||
</select>
|
||||
<small style="color: #666;">Equipment will be loaded based on selected station</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="data_type_id">Data Type:</label>
|
||||
<select id="data_type_id" name="data_type_id" required>
|
||||
<option value="">Select Data Type</option>
|
||||
</select>
|
||||
<small style="color: #666;">Data types will be loaded from tag metadata system</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol_address">Protocol Address:</label>
|
||||
|
|
|
|||
|
|
@ -179,10 +179,6 @@ class ProtocolDiscovery {
|
|||
<div class="alert alert-success">
|
||||
<i class="fas fa-check"></i>
|
||||
Discovery service ready
|
||||
${status.total_discovered_endpoints > 0 ?
|
||||
`- ${status.total_discovered_endpoints} endpoints discovered` :
|
||||
''
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
scanButton?.removeAttribute('disabled');
|
||||
|
|
@ -291,31 +287,29 @@ class ProtocolDiscovery {
|
|||
// Get station and pump info from form or prompt
|
||||
const stationId = document.getElementById('station-id')?.value || 'station_001';
|
||||
const pumpId = document.getElementById('pump-id')?.value || 'pump_001';
|
||||
const dataType = document.getElementById('data-type')?.value || 'setpoint';
|
||||
const dbSource = document.getElementById('db-source')?.value || 'frequency_hz';
|
||||
const dataType = document.getElementById('data-type')?.value || 'pressure';
|
||||
const dbSource = document.getElementById('db-source')?.value || 'influxdb';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/dashboard/discovery/apply/${this.currentScanId}`, {
|
||||
const response = await fetch(`/api/v1/dashboard/discovery/apply/${this.currentScanId}?station_id=${stationId}&pump_id=${pumpId}&data_type=${dataType}&db_source=${dbSource}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
station_id: stationId,
|
||||
pump_id: pumpId,
|
||||
data_type: dataType,
|
||||
db_source: dbSource
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.showNotification(`Successfully created ${result.created_mappings.length} protocol mappings`, 'success');
|
||||
|
||||
// Refresh protocol mappings grid
|
||||
if (window.protocolMappingGrid) {
|
||||
window.protocolMappingGrid.loadProtocolMappings();
|
||||
if (result.created_mappings.length > 0) {
|
||||
this.showNotification(`Successfully created ${result.created_mappings.length} protocol mappings from discovery results`, 'success');
|
||||
|
||||
// Refresh protocol mappings grid
|
||||
if (window.protocolMappingGrid) {
|
||||
window.protocolMappingGrid.loadProtocolMappings();
|
||||
}
|
||||
} else {
|
||||
this.showNotification('No protocol mappings were created. Check the discovery results for compatible endpoints.', 'warning');
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.detail || 'Failed to apply discovery results');
|
||||
|
|
@ -329,15 +323,163 @@ class ProtocolDiscovery {
|
|||
/**
|
||||
* Use discovered endpoint in protocol form
|
||||
*/
|
||||
useDiscoveredEndpoint(endpointId) {
|
||||
// This would fetch the specific endpoint details and populate the form
|
||||
// For now, we'll just show a notification
|
||||
this.showNotification(`Endpoint ${endpointId} selected for protocol mapping`, 'info');
|
||||
async useDiscoveredEndpoint(endpointId) {
|
||||
try {
|
||||
// Get current scan results to find the endpoint
|
||||
if (!this.currentScanId) {
|
||||
this.showNotification('No discovery results available', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/v1/dashboard/discovery/results/${this.currentScanId}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Failed to fetch discovery results');
|
||||
}
|
||||
|
||||
// Find the specific endpoint
|
||||
const endpoint = result.discovered_endpoints.find(ep => ep.device_id === endpointId);
|
||||
if (!endpoint) {
|
||||
this.showNotification(`Endpoint ${endpointId} not found in current scan`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate protocol mapping form with endpoint data
|
||||
this.populateProtocolForm(endpoint);
|
||||
|
||||
// Switch to protocol mapping tab
|
||||
this.switchToProtocolMappingTab();
|
||||
|
||||
this.showNotification(`Endpoint ${endpoint.device_name || endpointId} selected for protocol mapping`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error using discovered endpoint:', error);
|
||||
this.showNotification(`Failed to use endpoint: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate protocol mapping form with endpoint data
|
||||
*/
|
||||
populateProtocolForm(endpoint) {
|
||||
// Create a new protocol mapping ID
|
||||
const mappingId = `${endpoint.device_id}_${endpoint.protocol_type}`;
|
||||
|
||||
// In a real implementation, we would:
|
||||
// 1. Fetch endpoint details
|
||||
// 2. Populate protocol form fields
|
||||
// 3. Switch to protocol mapping tab
|
||||
// Set form values (these would be used when creating a new mapping)
|
||||
const formData = {
|
||||
mapping_id: mappingId,
|
||||
protocol_type: endpoint.protocol_type === 'opc_ua' ? 'opcua' : endpoint.protocol_type,
|
||||
protocol_address: this.getDefaultProtocolAddress(endpoint),
|
||||
device_name: endpoint.device_name || endpoint.device_id,
|
||||
device_address: endpoint.address,
|
||||
device_port: endpoint.port || '',
|
||||
station_id: 'station_001', // Default station ID
|
||||
equipment_id: 'equipment_001', // Default equipment ID
|
||||
data_type_id: 'datatype_001' // Default data type ID
|
||||
};
|
||||
|
||||
// Store form data for later use
|
||||
this.selectedEndpoint = formData;
|
||||
|
||||
// Show form data in console for debugging
|
||||
console.log('Protocol form populated with:', formData);
|
||||
|
||||
// Auto-populate the protocol mapping form
|
||||
this.autoPopulateProtocolForm(formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-populate the protocol mapping form with endpoint data
|
||||
*/
|
||||
autoPopulateProtocolForm(formData) {
|
||||
// First, open the "Add New Mapping" modal
|
||||
this.openAddMappingModal();
|
||||
|
||||
// Wait a moment for the modal to open, then populate fields
|
||||
setTimeout(() => {
|
||||
// Find and populate form fields in the modal
|
||||
const mappingIdField = document.getElementById('mapping-id');
|
||||
const protocolTypeField = document.getElementById('protocol-type');
|
||||
const protocolAddressField = document.getElementById('protocol-address');
|
||||
const deviceNameField = document.getElementById('device-name');
|
||||
const deviceAddressField = document.getElementById('device-address');
|
||||
const devicePortField = document.getElementById('device-port');
|
||||
const stationIdField = document.getElementById('station-id');
|
||||
const equipmentIdField = document.getElementById('equipment-id');
|
||||
const dataTypeIdField = document.getElementById('data-type-id');
|
||||
|
||||
if (mappingIdField) mappingIdField.value = formData.mapping_id;
|
||||
if (protocolTypeField) protocolTypeField.value = formData.protocol_type;
|
||||
if (protocolAddressField) protocolAddressField.value = formData.protocol_address;
|
||||
if (deviceNameField) deviceNameField.value = formData.device_name;
|
||||
if (deviceAddressField) deviceAddressField.value = formData.device_address;
|
||||
if (devicePortField) devicePortField.value = formData.device_port;
|
||||
if (stationIdField) stationIdField.value = formData.station_id;
|
||||
if (equipmentIdField) equipmentIdField.value = formData.equipment_id;
|
||||
if (dataTypeIdField) dataTypeIdField.value = formData.data_type_id;
|
||||
|
||||
// Show success message
|
||||
this.showNotification(`Protocol form populated with ${formData.device_name}. Please review and complete any missing information.`, 'success');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the "Add New Mapping" modal
|
||||
*/
|
||||
openAddMappingModal() {
|
||||
// Look for the showAddMappingModal function or button click
|
||||
if (typeof showAddMappingModal === 'function') {
|
||||
showAddMappingModal();
|
||||
} else {
|
||||
// Try to find and click the "Add New Mapping" button
|
||||
const addButton = document.querySelector('button[onclick*="showAddMappingModal"]');
|
||||
if (addButton) {
|
||||
addButton.click();
|
||||
} else {
|
||||
// Fallback: show a message to manually open the modal
|
||||
this.showNotification('Please click "Add New Mapping" to create a protocol mapping with the discovered endpoint data.', 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default protocol address based on endpoint type
|
||||
*/
|
||||
getDefaultProtocolAddress(endpoint) {
|
||||
const protocolType = endpoint.protocol_type;
|
||||
const deviceName = endpoint.device_name || endpoint.device_id;
|
||||
|
||||
switch (protocolType) {
|
||||
case 'modbus_tcp':
|
||||
return '40001'; // Default holding register
|
||||
case 'opc_ua':
|
||||
return `ns=2;s=${deviceName.replace(/\s+/g, '_')}`;
|
||||
case 'rest_api':
|
||||
return `http://${endpoint.address}${endpoint.port ? ':' + endpoint.port : ''}/api/data`;
|
||||
default:
|
||||
return endpoint.address;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to protocol mapping tab
|
||||
*/
|
||||
switchToProtocolMappingTab() {
|
||||
// Find and click the protocol mapping tab
|
||||
const mappingTab = document.querySelector('[data-tab="protocol-mapping"]');
|
||||
if (mappingTab) {
|
||||
mappingTab.click();
|
||||
} else {
|
||||
// Fallback: scroll to protocol mapping section
|
||||
const mappingSection = document.querySelector('#protocol-mapping-section');
|
||||
if (mappingSection) {
|
||||
mappingSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
// Show guidance message
|
||||
this.showNotification('Please complete the protocol mapping form with station, pump, and data type information', 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,100 @@
|
|||
// Protocol Mapping Functions
|
||||
let currentProtocolFilter = 'all';
|
||||
let editingMappingId = null;
|
||||
let tagMetadata = {
|
||||
stations: [],
|
||||
equipment: [],
|
||||
dataTypes: []
|
||||
};
|
||||
|
||||
// Tag Metadata Functions
|
||||
async function loadTagMetadata() {
|
||||
try {
|
||||
// Load stations
|
||||
const stationsResponse = await fetch('/api/v1/dashboard/metadata/stations');
|
||||
const stationsData = await stationsResponse.json();
|
||||
if (stationsData.success) {
|
||||
tagMetadata.stations = stationsData.stations;
|
||||
populateStationDropdown();
|
||||
}
|
||||
|
||||
// Load data types
|
||||
const dataTypesResponse = await fetch('/api/v1/dashboard/metadata/data-types');
|
||||
const dataTypesData = await dataTypesResponse.json();
|
||||
if (dataTypesData.success) {
|
||||
tagMetadata.dataTypes = dataTypesData.data_types;
|
||||
populateDataTypeDropdown();
|
||||
}
|
||||
|
||||
// Load equipment for all stations
|
||||
const equipmentResponse = await fetch('/api/v1/dashboard/metadata/equipment');
|
||||
const equipmentData = await equipmentResponse.json();
|
||||
if (equipmentData.success) {
|
||||
tagMetadata.equipment = equipmentData.equipment;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading tag metadata:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function populateStationDropdown() {
|
||||
const stationSelect = document.getElementById('station_id');
|
||||
stationSelect.innerHTML = '<option value="">Select Station</option>';
|
||||
|
||||
tagMetadata.stations.forEach(station => {
|
||||
const option = document.createElement('option');
|
||||
option.value = station.id;
|
||||
option.textContent = `${station.name} (${station.id})`;
|
||||
stationSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function populateEquipmentDropdown(stationId = null) {
|
||||
const equipmentSelect = document.getElementById('equipment_id');
|
||||
equipmentSelect.innerHTML = '<option value="">Select Equipment</option>';
|
||||
|
||||
let filteredEquipment = tagMetadata.equipment;
|
||||
if (stationId) {
|
||||
filteredEquipment = tagMetadata.equipment.filter(eq => eq.station_id === stationId);
|
||||
}
|
||||
|
||||
filteredEquipment.forEach(equipment => {
|
||||
const option = document.createElement('option');
|
||||
option.value = equipment.id;
|
||||
option.textContent = `${equipment.name} (${equipment.id})`;
|
||||
equipmentSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function populateDataTypeDropdown() {
|
||||
const dataTypeSelect = document.getElementById('data_type_id');
|
||||
dataTypeSelect.innerHTML = '<option value="">Select Data Type</option>';
|
||||
|
||||
tagMetadata.dataTypes.forEach(dataType => {
|
||||
const option = document.createElement('option');
|
||||
option.value = dataType.id;
|
||||
option.textContent = `${dataType.name} (${dataType.id})`;
|
||||
if (dataType.units) {
|
||||
option.textContent += ` [${dataType.units}]`;
|
||||
}
|
||||
dataTypeSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Event listener for station selection change
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const stationSelect = document.getElementById('station_id');
|
||||
if (stationSelect) {
|
||||
stationSelect.addEventListener('change', function() {
|
||||
const stationId = this.value;
|
||||
populateEquipmentDropdown(stationId);
|
||||
});
|
||||
}
|
||||
|
||||
// Load tag metadata when page loads
|
||||
loadTagMetadata();
|
||||
});
|
||||
|
||||
function selectProtocol(protocol) {
|
||||
currentProtocolFilter = protocol;
|
||||
|
|
@ -51,8 +145,8 @@ function displayProtocolMappings(mappings) {
|
|||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.id}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.protocol_type}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.station_id || '-'}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.pump_id || '-'}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.data_type}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.equipment_id || '-'}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.data_type_id || '-'}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.protocol_address}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.db_source}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">
|
||||
|
|
@ -77,9 +171,19 @@ function showEditMappingModal(mapping) {
|
|||
document.getElementById('modal-title').textContent = 'Edit Protocol Mapping';
|
||||
document.getElementById('mapping_id').value = mapping.id;
|
||||
document.getElementById('protocol_type').value = mapping.protocol_type;
|
||||
document.getElementById('station_id').value = mapping.station_id || '';
|
||||
document.getElementById('pump_id').value = mapping.pump_id || '';
|
||||
document.getElementById('data_type').value = mapping.data_type;
|
||||
|
||||
// Set dropdown values
|
||||
const stationSelect = document.getElementById('station_id');
|
||||
const equipmentSelect = document.getElementById('equipment_id');
|
||||
const dataTypeSelect = document.getElementById('data_type_id');
|
||||
|
||||
stationSelect.value = mapping.station_id || '';
|
||||
if (mapping.station_id) {
|
||||
populateEquipmentDropdown(mapping.station_id);
|
||||
}
|
||||
equipmentSelect.value = mapping.equipment_id || '';
|
||||
dataTypeSelect.value = mapping.data_type_id || '';
|
||||
|
||||
document.getElementById('protocol_address').value = mapping.protocol_address;
|
||||
document.getElementById('db_source').value = mapping.db_source;
|
||||
|
||||
|
|
@ -181,8 +285,8 @@ function getMappingFormData() {
|
|||
return {
|
||||
protocol_type: document.getElementById('protocol_type').value,
|
||||
station_id: document.getElementById('station_id').value,
|
||||
pump_id: document.getElementById('pump_id').value,
|
||||
data_type: document.getElementById('data_type').value,
|
||||
equipment_id: document.getElementById('equipment_id').value,
|
||||
data_type_id: document.getElementById('data_type_id').value,
|
||||
protocol_address: document.getElementById('protocol_address').value,
|
||||
db_source: document.getElementById('db_source').value
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Use Button Functionality</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.endpoint { border: 1px solid #ccc; padding: 10px; margin: 10px 0; }
|
||||
.use-btn { background: #007bff; color: white; border: none; padding: 5px 10px; cursor: pointer; }
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
|
||||
.modal-content { background: white; margin: 100px auto; padding: 20px; width: 500px; }
|
||||
.form-group { margin: 10px 0; }
|
||||
label { display: block; margin-bottom: 5px; }
|
||||
input, select { width: 100%; padding: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Use Button Functionality</h1>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Discovered Endpoint</h3>
|
||||
<p><strong>Device:</strong> Modbus Controller</p>
|
||||
<p><strong>Address:</strong> 192.168.1.100:502</p>
|
||||
<p><strong>Protocol:</strong> modbus_tcp</p>
|
||||
<p><strong>Node:</strong> 40001</p>
|
||||
<button class="use-btn" onclick="useEndpoint('modbus_tcp', '40001', 'Modbus Controller', '192.168.1.100', '502')">Use</button>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Discovered Endpoint</h3>
|
||||
<p><strong>Device:</strong> OPC UA Server</p>
|
||||
<p><strong>Address:</strong> 192.168.1.101:4840</p>
|
||||
<p><strong>Protocol:</strong> opcua</p>
|
||||
<p><strong>Node:</strong> ns=2;s=Pressure</p>
|
||||
<button class="use-btn" onclick="useEndpoint('opcua', 'ns=2;s=Pressure', 'OPC UA Server', '192.168.1.101', '4840')">Use</button>
|
||||
</div>
|
||||
|
||||
<!-- Add New Mapping Modal -->
|
||||
<div id="addMappingModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Add New Protocol Mapping</h2>
|
||||
<form id="protocolMappingForm">
|
||||
<div class="form-group">
|
||||
<label for="mapping-id">Mapping ID:</label>
|
||||
<input type="text" id="mapping-id" name="mapping-id">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol-type">Protocol Type:</label>
|
||||
<select id="protocol-type" name="protocol-type">
|
||||
<option value="modbus_tcp">Modbus TCP</option>
|
||||
<option value="opcua">OPC UA</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol-address">Protocol Address:</label>
|
||||
<input type="text" id="protocol-address" name="protocol-address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="device-name">Device Name:</label>
|
||||
<input type="text" id="device-name" name="device-name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="device-address">Device Address:</label>
|
||||
<input type="text" id="device-address" name="device-address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="device-port">Device Port:</label>
|
||||
<input type="text" id="device-port" name="device-port">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="button" onclick="closeModal()">Cancel</button>
|
||||
<button type="submit">Save Mapping</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Function to open the Add New Mapping modal
|
||||
function showAddMappingModal() {
|
||||
document.getElementById('addMappingModal').style.display = 'block';
|
||||
}
|
||||
|
||||
// Function to close the modal
|
||||
function closeModal() {
|
||||
document.getElementById('addMappingModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Function to use an endpoint (simulates the Use button)
|
||||
function useEndpoint(protocolType, protocolAddress, deviceName, deviceAddress, devicePort) {
|
||||
// First, open the Add New Mapping modal
|
||||
showAddMappingModal();
|
||||
|
||||
// Wait a moment for the modal to open, then populate fields
|
||||
setTimeout(() => {
|
||||
// Generate a mapping ID
|
||||
const mappingId = `${protocolType}_${deviceName.replace(/\s+/g, '_').toLowerCase()}_${Date.now()}`;
|
||||
|
||||
// Populate form fields
|
||||
document.getElementById('mapping-id').value = mappingId;
|
||||
document.getElementById('protocol-type').value = protocolType;
|
||||
document.getElementById('protocol-address').value = protocolAddress;
|
||||
document.getElementById('device-name').value = deviceName;
|
||||
document.getElementById('device-address').value = deviceAddress;
|
||||
document.getElementById('device-port').value = devicePort;
|
||||
|
||||
// Show success message
|
||||
alert(`Protocol form populated with ${deviceName}. Please complete station, equipment, and data type information.`);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('addMappingModal');
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('protocolMappingForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
alert('Protocol mapping would be saved here!');
|
||||
closeModal();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Use Button Workflow</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.card { border: 1px solid #ddd; border-radius: 5px; padding: 20px; margin: 10px 0; }
|
||||
.success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; }
|
||||
.error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; }
|
||||
.info { background-color: #d1ecf1; border-color: #bee5eb; color: #0c5460; }
|
||||
button { padding: 10px 15px; margin: 5px; border: none; border-radius: 4px; cursor: pointer; }
|
||||
.btn-primary { background-color: #007bff; color: white; }
|
||||
.btn-success { background-color: #28a745; color: white; }
|
||||
.form-group { margin: 10px 0; }
|
||||
label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||
input, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Test Use Button Workflow</h1>
|
||||
<p>This page tests the "Use" button functionality with the new tag-based metadata system.</p>
|
||||
|
||||
<div class="card info">
|
||||
<h3>Step 1: Simulate Discovery Results</h3>
|
||||
<p>Click the button below to simulate discovering a device endpoint:</p>
|
||||
<button id="simulate-discovery" class="btn-primary">Simulate Discovery</button>
|
||||
</div>
|
||||
|
||||
<div class="card" id="discovery-results" style="display: none;">
|
||||
<h3>Discovery Results</h3>
|
||||
<div id="endpoint-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="protocol-form" style="display: none;">
|
||||
<h3>Protocol Mapping Form (Auto-populated)</h3>
|
||||
<form id="mapping-form">
|
||||
<div class="form-group">
|
||||
<label for="mapping-id">Mapping ID:</label>
|
||||
<input type="text" id="mapping-id" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol-type">Protocol Type:</label>
|
||||
<input type="text" id="protocol-type" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol-address">Protocol Address:</label>
|
||||
<input type="text" id="protocol-address" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="station-id">Station ID:</label>
|
||||
<input type="text" id="station-id" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="equipment-id">Equipment ID:</label>
|
||||
<input type="text" id="equipment-id" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="data-type-id">Data Type ID:</label>
|
||||
<input type="text" id="data-type-id" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="db-source">Database Source:</label>
|
||||
<input type="text" id="db-source" value="pump_data.speed">
|
||||
</div>
|
||||
<button type="button" id="create-mapping" class="btn-success">Create Protocol Mapping</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" id="result-message" style="display: none;">
|
||||
<h3>Result</h3>
|
||||
<div id="result-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simulate discovery results
|
||||
document.getElementById('simulate-discovery').addEventListener('click', function() {
|
||||
const endpoints = [
|
||||
{
|
||||
device_id: 'device_001',
|
||||
protocol_type: 'modbus_tcp',
|
||||
device_name: 'Test Pump Controller',
|
||||
address: '192.168.1.100',
|
||||
port: 502,
|
||||
capabilities: ['read_holding_registers', 'write_holding_registers'],
|
||||
discovered_at: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
// Display discovery results
|
||||
const endpointList = document.getElementById('endpoint-list');
|
||||
endpointList.innerHTML = `
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background-color: #f8f9fa;">
|
||||
<th style="padding: 8px; border: 1px solid #ddd;">Device Name</th>
|
||||
<th style="padding: 8px; border: 1px solid #ddd;">Protocol</th>
|
||||
<th style="padding: 8px; border: 1px solid #ddd;">Address</th>
|
||||
<th style="padding: 8px; border: 1px solid #ddd;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${endpoints.map(endpoint => `
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">${endpoint.device_name}</td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">${endpoint.protocol_type}</td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">${endpoint.address}:${endpoint.port}</td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">
|
||||
<button class="use-endpoint" data-endpoint='${JSON.stringify(endpoint)}'>Use</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
document.getElementById('discovery-results').style.display = 'block';
|
||||
|
||||
// Add event listeners to Use buttons
|
||||
document.querySelectorAll('.use-endpoint').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const endpoint = JSON.parse(this.getAttribute('data-endpoint'));
|
||||
populateProtocolForm(endpoint);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Populate protocol form with endpoint data
|
||||
function populateProtocolForm(endpoint) {
|
||||
const mappingId = `${endpoint.device_id}_${endpoint.protocol_type}`;
|
||||
|
||||
// Set form values
|
||||
document.getElementById('mapping-id').value = mappingId;
|
||||
document.getElementById('protocol-type').value = endpoint.protocol_type;
|
||||
document.getElementById('protocol-address').value = getDefaultProtocolAddress(endpoint);
|
||||
document.getElementById('station-id').value = 'station_001';
|
||||
document.getElementById('equipment-id').value = 'equipment_001';
|
||||
document.getElementById('data-type-id').value = 'datatype_001';
|
||||
|
||||
// Show the form
|
||||
document.getElementById('protocol-form').style.display = 'block';
|
||||
|
||||
// Scroll to form
|
||||
document.getElementById('protocol-form').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Get default protocol address
|
||||
function getDefaultProtocolAddress(endpoint) {
|
||||
switch (endpoint.protocol_type) {
|
||||
case 'modbus_tcp':
|
||||
return '40001';
|
||||
case 'opc_ua':
|
||||
return `ns=2;s=${endpoint.device_name.replace(/\s+/g, '_')}`;
|
||||
case 'rest_api':
|
||||
return `http://${endpoint.address}${endpoint.port ? ':' + endpoint.port : ''}/api/data`;
|
||||
default:
|
||||
return endpoint.address;
|
||||
}
|
||||
}
|
||||
|
||||
// Create protocol mapping
|
||||
document.getElementById('create-mapping').addEventListener('click', async function() {
|
||||
const formData = {
|
||||
protocol_type: document.getElementById('protocol-type').value,
|
||||
station_id: document.getElementById('station-id').value,
|
||||
equipment_id: document.getElementById('equipment-id').value,
|
||||
data_type_id: document.getElementById('data-type-id').value,
|
||||
protocol_address: document.getElementById('protocol-address').value,
|
||||
db_source: document.getElementById('db-source').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('http://95.111.206.155:8081/api/v1/dashboard/protocol-mappings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
const resultDiv = document.getElementById('result-content');
|
||||
const resultCard = document.getElementById('result-message');
|
||||
|
||||
if (response.ok && result.success) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="success">
|
||||
<h4>✅ Success!</h4>
|
||||
<p>Protocol mapping created successfully:</p>
|
||||
<ul>
|
||||
<li><strong>ID:</strong> ${result.mapping.id}</li>
|
||||
<li><strong>Station:</strong> ${result.mapping.station_id}</li>
|
||||
<li><strong>Equipment:</strong> ${result.mapping.equipment_id}</li>
|
||||
<li><strong>Data Type:</strong> ${result.mapping.data_type_id}</li>
|
||||
<li><strong>Protocol:</strong> ${result.mapping.protocol_type}</li>
|
||||
<li><strong>Address:</strong> ${result.mapping.protocol_address}</li>
|
||||
<li><strong>DB Source:</strong> ${result.mapping.db_source}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
resultCard.style.display = 'block';
|
||||
} else {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="error">
|
||||
<h4>❌ Error</h4>
|
||||
<p>Failed to create protocol mapping:</p>
|
||||
<p><strong>Status:</strong> ${response.status}</p>
|
||||
<p><strong>Error:</strong> ${result.detail || 'Unknown error'}</p>
|
||||
</div>
|
||||
`;
|
||||
resultCard.style.display = 'block';
|
||||
}
|
||||
|
||||
resultCard.scrollIntoView({ behavior: 'smooth' });
|
||||
|
||||
} catch (error) {
|
||||
const resultDiv = document.getElementById('result-content');
|
||||
const resultCard = document.getElementById('result-message');
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div class="error">
|
||||
<h4>❌ Network Error</h4>
|
||||
<p>Failed to connect to server:</p>
|
||||
<p><strong>Error:</strong> ${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
resultCard.style.display = 'block';
|
||||
resultCard.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue