Complete legacy system removal and tag metadata integration

- Remove legacy configuration classes: PumpStationConfig, PumpConfig, SafetyLimitsConfig
- Update ProtocolMapping model with tag metadata validators
- Replace text inputs with dropdowns in UI templates
- Add tag metadata loading functions to JavaScript
- Remove legacy API endpoints and add tag metadata endpoints
- Update security permissions to remove configure_safety_limits
- Clean up configuration manager and hardware discovery
- All integration tests pass successfully

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands 2025-11-08 10:31:36 +00:00
parent c741ac8553
commit 5a2cdc2324
11 changed files with 1706 additions and 269 deletions

View File

@ -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

View File

@ -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()

View File

@ -236,7 +236,6 @@ class AuthorizationManager:
"emergency_stop", "emergency_stop",
"clear_emergency_stop", "clear_emergency_stop",
"view_alerts", "view_alerts",
"configure_safety_limits",
"manage_pump_configuration", "manage_pump_configuration",
"view_system_metrics" "view_system_metrics"
}, },
@ -247,7 +246,6 @@ class AuthorizationManager:
"emergency_stop", "emergency_stop",
"clear_emergency_stop", "clear_emergency_stop",
"view_alerts", "view_alerts",
"configure_safety_limits",
"manage_pump_configuration", "manage_pump_configuration",
"view_system_metrics", "view_system_metrics",
"manage_users", "manage_users",

View File

@ -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()

View File

@ -12,10 +12,10 @@ from pydantic import BaseModel, ValidationError
from config.settings import Settings from config.settings import Settings
from .configuration_manager import ( from .configuration_manager import (
configuration_manager, OPCUAConfig, ModbusTCPConfig, PumpStationConfig, configuration_manager, OPCUAConfig, ModbusTCPConfig, DataPointMapping, ProtocolType, ProtocolMapping
PumpConfig, SafetyLimitsConfig, DataPointMapping, ProtocolType, ProtocolMapping
) )
from src.discovery.protocol_discovery_persistent import persistent_discovery_service, DiscoveryStatus, DiscoveredEndpoint 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 from datetime import datetime
logger = logging.getLogger(__name__) 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)}") 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)}") 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") @dashboard_router.post("/configure/data-mapping")
async def configure_data_mapping(mapping: DataPointMapping): async def configure_data_mapping(mapping: DataPointMapping):
@ -830,13 +793,13 @@ async def export_signals():
async def get_protocol_mappings( async def get_protocol_mappings(
protocol_type: Optional[str] = None, protocol_type: Optional[str] = None,
station_id: Optional[str] = None, station_id: Optional[str] = None,
pump_id: Optional[str] = None equipment_id: Optional[str] = None
): ):
"""Get protocol mappings with optional filtering""" """Get protocol mappings with optional filtering"""
try: try:
# Convert protocol_type string to enum if provided # Convert protocol_type string to enum if provided
protocol_enum = None protocol_enum = None
if protocol_type: if protocol_type and protocol_type != "all":
try: try:
protocol_enum = ProtocolType(protocol_type) protocol_enum = ProtocolType(protocol_type)
except ValueError: except ValueError:
@ -845,7 +808,7 @@ async def get_protocol_mappings(
mappings = configuration_manager.get_protocol_mappings( mappings = configuration_manager.get_protocol_mappings(
protocol_type=protocol_enum, protocol_type=protocol_enum,
station_id=station_id, station_id=station_id,
pump_id=pump_id equipment_id=equipment_id
) )
return { return {
@ -873,14 +836,19 @@ async def create_protocol_mapping(mapping_data: dict):
# Create ProtocolMapping object # Create ProtocolMapping object
import uuid import uuid
mapping = ProtocolMapping( 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, protocol_type=protocol_enum,
station_id=mapping_data.get("station_id"), station_id=mapping_data.get("station_id"),
pump_id=mapping_data.get("pump_id"), equipment_id=mapping_data.get("equipment_id"),
data_type=mapping_data.get("data_type"), data_type_id=mapping_data.get("data_type_id"),
protocol_address=mapping_data.get("protocol_address"), protocol_address=mapping_data.get("protocol_address"),
db_source=mapping_data.get("db_source"), db_source=mapping_data.get("db_source"),
transformation_rules=mapping_data.get("transformation_rules", []), 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"), modbus_config=mapping_data.get("modbus_config"),
opcua_config=mapping_data.get("opcua_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 id=mapping_id, # Use the ID from URL
protocol_type=protocol_enum or ProtocolType(mapping_data.get("protocol_type")), protocol_type=protocol_enum or ProtocolType(mapping_data.get("protocol_type")),
station_id=mapping_data.get("station_id"), station_id=mapping_data.get("station_id"),
pump_id=mapping_data.get("pump_id"), equipment_id=mapping_data.get("equipment_id"),
data_type=mapping_data.get("data_type"), data_type_id=mapping_data.get("data_type_id"),
protocol_address=mapping_data.get("protocol_address"), protocol_address=mapping_data.get("protocol_address"),
db_source=mapping_data.get("db_source"), db_source=mapping_data.get("db_source"),
transformation_rules=mapping_data.get("transformation_rules", []), transformation_rules=mapping_data.get("transformation_rules", []),
@ -971,6 +939,181 @@ async def delete_protocol_mapping(mapping_id: str):
# Protocol Discovery API Endpoints # 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") @dashboard_router.get("/discovery/status")
async def get_discovery_status(): async def get_discovery_status():
"""Get current discovery service status""" """Get current discovery service status"""
@ -1097,7 +1240,7 @@ async def get_recent_discoveries():
@dashboard_router.post("/discovery/apply/{scan_id}") @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""" """Apply discovered endpoints as protocol mappings"""
try: try:
result = persistent_discovery_service.get_scan_result(scan_id) 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", []): for endpoint in result.get("discovered_endpoints", []):
try: try:
# Create protocol mapping from discovered endpoint # 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( protocol_mapping = ProtocolMapping(
id=mapping_id, id=mapping_id,
station_id=station_id, station_id=station_id,
pump_id=pump_id, equipment_id=equipment_id,
protocol_type=endpoint.get("protocol_type"), protocol_type=protocol_type,
protocol_address=endpoint.get("address"), protocol_address=protocol_address,
data_type=data_type, data_type_id=data_type_id,
db_source=db_source db_source=db_source
) )
@ -1167,8 +1324,8 @@ async def validate_protocol_mapping(mapping_id: str, mapping_data: dict):
id=mapping_id, id=mapping_id,
protocol_type=protocol_enum, protocol_type=protocol_enum,
station_id=mapping_data.get("station_id"), station_id=mapping_data.get("station_id"),
pump_id=mapping_data.get("pump_id"), equipment_id=mapping_data.get("equipment_id"),
data_type=mapping_data.get("data_type"), data_type_id=mapping_data.get("data_type_id"),
protocol_address=mapping_data.get("protocol_address"), protocol_address=mapping_data.get("protocol_address"),
db_source=mapping_data.get("db_source"), db_source=mapping_data.get("db_source"),
transformation_rules=mapping_data.get("transformation_rules", []), transformation_rules=mapping_data.get("transformation_rules", []),

View File

@ -52,57 +52,7 @@ class ModbusTCPConfig(SCADAProtocolConfig):
raise ValueError("Port must be between 1 and 65535") raise ValueError("Port must be between 1 and 65535")
return v 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): class DataPointMapping(BaseModel):
"""Data point mapping between protocol and internal representation""" """Data point mapping between protocol and internal representation"""
@ -118,12 +68,19 @@ class ProtocolMapping(BaseModel):
id: str id: str
protocol_type: ProtocolType protocol_type: ProtocolType
station_id: str station_id: str
pump_id: str equipment_id: str
data_type: str # setpoint, status, power, flow, level, safety, etc. data_type_id: str
protocol_address: str # register address or OPC UA node protocol_address: str # register address or OPC UA node
db_source: str # database table and column db_source: str # database table and column
transformation_rules: List[Dict[str, Any]] = [] 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 # Protocol-specific configurations
modbus_config: Optional[Dict[str, Any]] = None modbus_config: Optional[Dict[str, Any]] = None
opcua_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") raise ValueError("Mapping ID must be alphanumeric with underscores")
return v 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') @validator('protocol_address')
def validate_protocol_address(cls, v, values): def validate_protocol_address(cls, v, values):
if 'protocol_type' in values: if 'protocol_type' in values:
@ -159,11 +146,57 @@ class ProtocolMapping(BaseModel):
raise ValueError("REST API endpoint must start with 'http://' or 'https://'") raise ValueError("REST API endpoint must start with 'http://' or 'https://'")
return v 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): class HardwareDiscoveryResult(BaseModel):
"""Result from hardware auto-discovery""" """Result from hardware auto-discovery"""
success: bool success: bool
discovered_stations: List[PumpStationConfig] = [] discovered_stations: List[Dict[str, Any]] = []
discovered_pumps: List[PumpConfig] = [] discovered_pumps: List[Dict[str, Any]] = []
errors: List[str] = [] errors: List[str] = []
warnings: List[str] = [] warnings: List[str] = []
@ -172,9 +205,6 @@ class ConfigurationManager:
def __init__(self, db_client=None): def __init__(self, db_client=None):
self.protocol_configs: Dict[ProtocolType, SCADAProtocolConfig] = {} 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.data_mappings: List[DataPointMapping] = []
self.protocol_mappings: List[ProtocolMapping] = [] self.protocol_mappings: List[ProtocolMapping] = []
self.db_client = db_client self.db_client = db_client
@ -187,11 +217,11 @@ class ConfigurationManager:
"""Load protocol mappings from database""" """Load protocol mappings from database"""
try: try:
query = """ query = """
SELECT mapping_id, station_id, pump_id, protocol_type, SELECT mapping_id, station_id, equipment_id, protocol_type,
protocol_address, data_type, db_source, enabled protocol_address, data_type_id, db_source, enabled
FROM protocol_mappings FROM protocol_mappings
WHERE enabled = true 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) results = self.db_client.execute_query(query)
@ -205,10 +235,10 @@ class ConfigurationManager:
mapping = ProtocolMapping( mapping = ProtocolMapping(
id=row['mapping_id'], id=row['mapping_id'],
station_id=row['station_id'], station_id=row['station_id'],
pump_id=row['pump_id'], equipment_id=row['equipment_id'],
protocol_type=protocol_type, protocol_type=protocol_type,
protocol_address=row['protocol_address'], protocol_address=row['protocol_address'],
data_type=row['data_type'], data_type_id=row['data_type_id'],
db_source=row['db_source'] db_source=row['db_source']
) )
self.protocol_mappings.append(mapping) self.protocol_mappings.append(mapping)
@ -230,44 +260,7 @@ class ConfigurationManager:
logger.error(f"Failed to configure protocol {config.protocol_type}: {str(e)}") logger.error(f"Failed to configure protocol {config.protocol_type}: {str(e)}")
return False 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: def map_data_point(self, mapping: DataPointMapping) -> bool:
"""Map a data point between protocol and internal representation""" """Map a data point between protocol and internal representation"""
@ -307,14 +300,14 @@ class ConfigurationManager:
if self.db_client: if self.db_client:
query = """ query = """
INSERT INTO protocol_mappings INSERT INTO protocol_mappings
(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, :pump_id, :protocol_type, :protocol_address, :data_type, :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 ON CONFLICT (mapping_id) DO UPDATE SET
station_id = EXCLUDED.station_id, station_id = EXCLUDED.station_id,
pump_id = EXCLUDED.pump_id, equipment_id = EXCLUDED.equipment_id,
protocol_type = EXCLUDED.protocol_type, protocol_type = EXCLUDED.protocol_type,
protocol_address = EXCLUDED.protocol_address, protocol_address = EXCLUDED.protocol_address,
data_type = EXCLUDED.data_type, data_type_id = EXCLUDED.data_type_id,
db_source = EXCLUDED.db_source, db_source = EXCLUDED.db_source,
enabled = EXCLUDED.enabled, enabled = EXCLUDED.enabled,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
@ -322,10 +315,10 @@ class ConfigurationManager:
params = { params = {
'mapping_id': mapping.id, 'mapping_id': mapping.id,
'station_id': mapping.station_id, 'station_id': mapping.station_id,
'pump_id': mapping.pump_id, 'equipment_id': mapping.equipment_id,
'protocol_type': mapping.protocol_type.value, 'protocol_type': mapping.protocol_type.value,
'protocol_address': mapping.protocol_address, 'protocol_address': mapping.protocol_address,
'data_type': mapping.data_type, 'data_type_id': mapping.data_type_id,
'db_source': mapping.db_source, 'db_source': mapping.db_source,
'created_by': 'dashboard', 'created_by': 'dashboard',
'enabled': True 'enabled': True
@ -333,7 +326,7 @@ class ConfigurationManager:
self.db_client.execute(query, params) self.db_client.execute(query, params)
self.protocol_mappings.append(mapping) 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 return True
except Exception as e: except Exception as e:
logger.error(f"Failed to add protocol mapping {mapping.id}: {str(e)}") logger.error(f"Failed to add protocol mapping {mapping.id}: {str(e)}")
@ -342,8 +335,8 @@ class ConfigurationManager:
def get_protocol_mappings(self, def get_protocol_mappings(self,
protocol_type: Optional[ProtocolType] = None, protocol_type: Optional[ProtocolType] = None,
station_id: Optional[str] = None, station_id: Optional[str] = None,
pump_id: Optional[str] = None) -> List[ProtocolMapping]: equipment_id: Optional[str] = None) -> List[ProtocolMapping]:
"""Get mappings filtered by protocol/station/pump""" """Get mappings filtered by protocol/station/equipment"""
filtered_mappings = self.protocol_mappings.copy() filtered_mappings = self.protocol_mappings.copy()
if protocol_type: if protocol_type:
@ -352,8 +345,8 @@ class ConfigurationManager:
if station_id: if station_id:
filtered_mappings = [m for m in filtered_mappings if m.station_id == station_id] filtered_mappings = [m for m in filtered_mappings if m.station_id == station_id]
if pump_id: if equipment_id:
filtered_mappings = [m for m in filtered_mappings if m.pump_id == pump_id] filtered_mappings = [m for m in filtered_mappings if m.equipment_id == equipment_id]
return filtered_mappings return filtered_mappings
@ -373,10 +366,10 @@ class ConfigurationManager:
query = """ query = """
UPDATE protocol_mappings UPDATE protocol_mappings
SET station_id = :station_id, SET station_id = :station_id,
pump_id = :pump_id, equipment_id = :equipment_id,
protocol_type = :protocol_type, protocol_type = :protocol_type,
protocol_address = :protocol_address, protocol_address = :protocol_address,
data_type = :data_type, data_type_id = :data_type_id,
db_source = :db_source, db_source = :db_source,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE mapping_id = :mapping_id WHERE mapping_id = :mapping_id
@ -384,10 +377,10 @@ class ConfigurationManager:
params = { params = {
'mapping_id': mapping_id, 'mapping_id': mapping_id,
'station_id': updated_mapping.station_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_type': updated_mapping.protocol_type.value,
'protocol_address': updated_mapping.protocol_address, '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 'db_source': updated_mapping.db_source
} }
self.db_client.execute(query, params) self.db_client.execute(query, params)
@ -445,7 +438,7 @@ class ConfigurationManager:
if (existing.id != mapping.id and if (existing.id != mapping.id and
existing.protocol_type == ProtocolType.MODBUS_TCP and existing.protocol_type == ProtocolType.MODBUS_TCP and
existing.protocol_address == mapping.protocol_address): 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 break
except ValueError: except ValueError:
@ -461,7 +454,7 @@ class ConfigurationManager:
if (existing.id != mapping.id and if (existing.id != mapping.id and
existing.protocol_type == ProtocolType.OPC_UA and existing.protocol_type == ProtocolType.OPC_UA and
existing.protocol_address == mapping.protocol_address): 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 break
elif mapping.protocol_type == ProtocolType.MODBUS_RTU: elif mapping.protocol_type == ProtocolType.MODBUS_RTU:
@ -476,7 +469,7 @@ class ConfigurationManager:
if (existing.id != mapping.id and if (existing.id != mapping.id and
existing.protocol_type == ProtocolType.MODBUS_RTU and existing.protocol_type == ProtocolType.MODBUS_RTU and
existing.protocol_address == mapping.protocol_address): 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 break
except ValueError: except ValueError:
@ -492,7 +485,7 @@ class ConfigurationManager:
if (existing.id != mapping.id and if (existing.id != mapping.id and
existing.protocol_type == ProtocolType.REST_API and existing.protocol_type == ProtocolType.REST_API and
existing.protocol_address == mapping.protocol_address): 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 break
# Check database source format # Check database source format
@ -517,25 +510,25 @@ class ConfigurationManager:
if ProtocolType.OPC_UA in self.protocol_configs: if ProtocolType.OPC_UA in self.protocol_configs:
logger.info("Performing OPC UA hardware discovery...") logger.info("Performing OPC UA hardware discovery...")
# Simulate discovering a station via OPC UA # Simulate discovering a station via OPC UA
mock_station = PumpStationConfig( mock_station = {
station_id="discovered_station_001", "station_id": "discovered_station_001",
name="Discovered Pump Station", "name": "Discovered Pump Station",
location="Building A", "location": "Building A",
max_pumps=2, "max_pumps": 2,
power_capacity=100.0 "power_capacity": 100.0
) }
result.discovered_stations.append(mock_station) result.discovered_stations.append(mock_station)
# Simulate discovering pumps # Simulate discovering pumps
mock_pump = PumpConfig( mock_pump = {
pump_id="discovered_pump_001", "pump_id": "discovered_pump_001",
station_id="discovered_station_001", "station_id": "discovered_station_001",
name="Discovered Primary Pump", "name": "Discovered Primary Pump",
type="centrifugal", "type": "centrifugal",
power_rating=55.0, "power_rating": 55.0,
max_speed=50.0, "max_speed": 50.0,
min_speed=20.0 "min_speed": 20.0
) }
result.discovered_pumps.append(mock_pump) result.discovered_pumps.append(mock_pump)
# Mock Modbus discovery # Mock Modbus discovery
@ -592,9 +585,6 @@ class ConfigurationManager:
# Create summary # Create summary
validation_result["summary"] = { validation_result["summary"] = {
"protocols_configured": len(self.protocol_configs), "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), "data_mappings": len(self.data_mappings),
"protocol_mappings": len(self.protocol_mappings) "protocol_mappings": len(self.protocol_mappings)
} }
@ -605,9 +595,6 @@ class ConfigurationManager:
"""Export complete configuration for backup""" """Export complete configuration for backup"""
return { return {
"protocols": {pt.value: config.dict() for pt, config in self.protocol_configs.items()}, "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], "data_mappings": [mapping.dict() for mapping in self.data_mappings],
"protocol_mappings": [mapping.dict() for mapping in self.protocol_mappings] "protocol_mappings": [mapping.dict() for mapping in self.protocol_mappings]
} }
@ -617,9 +604,6 @@ class ConfigurationManager:
try: try:
# Clear existing configuration # Clear existing configuration
self.protocol_configs.clear() self.protocol_configs.clear()
self.stations.clear()
self.pumps.clear()
self.safety_limits.clear()
self.data_mappings.clear() self.data_mappings.clear()
self.protocol_mappings.clear() self.protocol_mappings.clear()
@ -634,21 +618,6 @@ class ConfigurationManager:
config = SCADAProtocolConfig(**config_dict) config = SCADAProtocolConfig(**config_dict)
self.protocol_configs[protocol_type] = config 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 # Import data mappings
for mapping_dict in config_data.get("data_mappings", []): for mapping_dict in config_data.get("data_mappings", []):
mapping = DataPointMapping(**mapping_dict) mapping = DataPointMapping(**mapping_dict)

View File

@ -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;">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;">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;">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;">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;">Protocol Address</th>
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Database Source</th> <th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Database Source</th>
@ -599,25 +599,25 @@ DASHBOARD_HTML = """
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="station_id">Station ID:</label> <label for="station_id">Station:</label>
<input type="text" id="station_id" name="station_id" required> <select id="station_id" name="station_id" required>
</div> <option value="">Select Station</option>
<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>
</select> </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>
<div class="form-group"> <div class="form-group">
<label for="protocol_address">Protocol Address:</label> <label for="protocol_address">Protocol Address:</label>

View File

@ -179,10 +179,6 @@ class ProtocolDiscovery {
<div class="alert alert-success"> <div class="alert alert-success">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
Discovery service ready Discovery service ready
${status.total_discovered_endpoints > 0 ?
`- ${status.total_discovered_endpoints} endpoints discovered` :
''
}
</div> </div>
`; `;
scanButton?.removeAttribute('disabled'); scanButton?.removeAttribute('disabled');
@ -291,32 +287,30 @@ class ProtocolDiscovery {
// Get station and pump info from form or prompt // Get station and pump info from form or prompt
const stationId = document.getElementById('station-id')?.value || 'station_001'; const stationId = document.getElementById('station-id')?.value || 'station_001';
const pumpId = document.getElementById('pump-id')?.value || 'pump_001'; const pumpId = document.getElementById('pump-id')?.value || 'pump_001';
const dataType = document.getElementById('data-type')?.value || 'setpoint'; const dataType = document.getElementById('data-type')?.value || 'pressure';
const dbSource = document.getElementById('db-source')?.value || 'frequency_hz'; const dbSource = document.getElementById('db-source')?.value || 'influxdb';
try { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, }
body: JSON.stringify({
station_id: stationId,
pump_id: pumpId,
data_type: dataType,
db_source: dbSource
})
}); });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
this.showNotification(`Successfully created ${result.created_mappings.length} protocol mappings`, 'success'); if (result.created_mappings.length > 0) {
this.showNotification(`Successfully created ${result.created_mappings.length} protocol mappings from discovery results`, 'success');
// Refresh protocol mappings grid // Refresh protocol mappings grid
if (window.protocolMappingGrid) { if (window.protocolMappingGrid) {
window.protocolMappingGrid.loadProtocolMappings(); window.protocolMappingGrid.loadProtocolMappings();
} }
} else {
this.showNotification('No protocol mappings were created. Check the discovery results for compatible endpoints.', 'warning');
}
} else { } else {
throw new Error(result.detail || 'Failed to apply discovery results'); throw new Error(result.detail || 'Failed to apply discovery results');
} }
@ -329,15 +323,163 @@ class ProtocolDiscovery {
/** /**
* Use discovered endpoint in protocol form * Use discovered endpoint in protocol form
*/ */
useDiscoveredEndpoint(endpointId) { async useDiscoveredEndpoint(endpointId) {
// This would fetch the specific endpoint details and populate the form try {
// For now, we'll just show a notification // Get current scan results to find the endpoint
this.showNotification(`Endpoint ${endpointId} selected for protocol mapping`, 'info'); if (!this.currentScanId) {
this.showNotification('No discovery results available', 'warning');
return;
}
// In a real implementation, we would: const response = await fetch(`/api/v1/dashboard/discovery/results/${this.currentScanId}`);
// 1. Fetch endpoint details const result = await response.json();
// 2. Populate protocol form fields
// 3. Switch to protocol mapping tab 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}`;
// 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');
} }
/** /**

View File

@ -1,6 +1,100 @@
// Protocol Mapping Functions // Protocol Mapping Functions
let currentProtocolFilter = 'all'; let currentProtocolFilter = 'all';
let editingMappingId = null; 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) { function selectProtocol(protocol) {
currentProtocolFilter = 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.id}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.protocol_type}</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.station_id || '-'}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.pump_id || '-'}</td> <td style="padding: 10px; border: 1px solid #ddd;">${mapping.equipment_id || '-'}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.data_type}</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.protocol_address}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.db_source}</td> <td style="padding: 10px; border: 1px solid #ddd;">${mapping.db_source}</td>
<td style="padding: 10px; border: 1px solid #ddd;"> <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('modal-title').textContent = 'Edit Protocol Mapping';
document.getElementById('mapping_id').value = mapping.id; document.getElementById('mapping_id').value = mapping.id;
document.getElementById('protocol_type').value = mapping.protocol_type; document.getElementById('protocol_type').value = mapping.protocol_type;
document.getElementById('station_id').value = mapping.station_id || '';
document.getElementById('pump_id').value = mapping.pump_id || ''; // Set dropdown values
document.getElementById('data_type').value = mapping.data_type; 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('protocol_address').value = mapping.protocol_address;
document.getElementById('db_source').value = mapping.db_source; document.getElementById('db_source').value = mapping.db_source;
@ -181,8 +285,8 @@ function getMappingFormData() {
return { return {
protocol_type: document.getElementById('protocol_type').value, protocol_type: document.getElementById('protocol_type').value,
station_id: document.getElementById('station_id').value, station_id: document.getElementById('station_id').value,
pump_id: document.getElementById('pump_id').value, equipment_id: document.getElementById('equipment_id').value,
data_type: document.getElementById('data_type').value, data_type_id: document.getElementById('data_type_id').value,
protocol_address: document.getElementById('protocol_address').value, protocol_address: document.getElementById('protocol_address').value,
db_source: document.getElementById('db_source').value db_source: document.getElementById('db_source').value
}; };

127
test_use_button.html Normal file
View File

@ -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>

View File

@ -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>