feat: Implement configurable pump control preprocessing logic #5

Merged
solipsism merged 34 commits from feature/pump-control-preprocessing into master 2025-11-17 14:23:42 +00:00
17 changed files with 4266 additions and 984 deletions
Showing only changes of commit f0d6aca5ed - Show all commits

View File

@ -0,0 +1,221 @@
-- Calejo Control Simplified Schema Migration
-- Migration from complex ID system to simple signal names + tags
-- Date: November 8, 2025
-- =============================================
-- STEP 1: Create new simplified tables
-- =============================================
-- New simplified protocol_signals table
CREATE TABLE IF NOT EXISTS protocol_signals (
signal_id VARCHAR(100) PRIMARY KEY,
signal_name VARCHAR(200) NOT NULL,
tags TEXT[] NOT NULL DEFAULT '{}',
protocol_type VARCHAR(20) NOT NULL,
protocol_address VARCHAR(500) NOT NULL,
db_source VARCHAR(100) NOT NULL,
-- Signal preprocessing configuration
preprocessing_enabled BOOLEAN DEFAULT FALSE,
preprocessing_rules JSONB,
min_output_value DECIMAL(10, 4),
max_output_value DECIMAL(10, 4),
default_output_value DECIMAL(10, 4),
-- Protocol-specific configurations
modbus_config JSONB,
opcua_config JSONB,
-- Metadata
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
created_by VARCHAR(100),
enabled BOOLEAN DEFAULT TRUE,
-- Constraints
CONSTRAINT valid_protocol_type CHECK (protocol_type IN ('opcua', 'modbus_tcp', 'modbus_rtu', 'rest_api')),
CONSTRAINT signal_name_not_empty CHECK (signal_name <> ''),
CONSTRAINT valid_signal_id CHECK (signal_id ~ '^[a-zA-Z0-9_-]+$')
);
COMMENT ON TABLE protocol_signals IS 'Simplified protocol signals with human-readable names and tags';
COMMENT ON COLUMN protocol_signals.signal_id IS 'Unique identifier for the signal';
COMMENT ON COLUMN protocol_signals.signal_name IS 'Human-readable signal name';
COMMENT ON COLUMN protocol_signals.tags IS 'Array of tags for categorization and filtering';
COMMENT ON COLUMN protocol_signals.protocol_type IS 'Protocol type: opcua, modbus_tcp, modbus_rtu, rest_api';
COMMENT ON COLUMN protocol_signals.protocol_address IS 'Protocol-specific address (OPC UA node ID, Modbus register, REST endpoint)';
COMMENT ON COLUMN protocol_signals.db_source IS 'Database field name that this signal represents';
-- Create indexes for efficient querying
CREATE INDEX idx_protocol_signals_tags ON protocol_signals USING GIN(tags);
CREATE INDEX idx_protocol_signals_protocol_type ON protocol_signals(protocol_type, enabled);
CREATE INDEX idx_protocol_signals_signal_name ON protocol_signals(signal_name);
CREATE INDEX idx_protocol_signals_created_at ON protocol_signals(created_at DESC);
-- =============================================
-- STEP 2: Migration function to convert existing data
-- =============================================
CREATE OR REPLACE FUNCTION migrate_protocol_mappings_to_signals()
RETURNS INTEGER AS $$
DECLARE
migrated_count INTEGER := 0;
mapping_record RECORD;
station_name_text TEXT;
pump_name_text TEXT;
signal_name_text TEXT;
tags_array TEXT[];
signal_id_text TEXT;
BEGIN
-- Loop through existing protocol mappings
FOR mapping_record IN
SELECT
pm.mapping_id,
pm.station_id,
pm.pump_id,
pm.protocol_type,
pm.protocol_address,
pm.data_type,
pm.db_source,
ps.station_name,
p.pump_name
FROM protocol_mappings pm
LEFT JOIN pump_stations ps ON pm.station_id = ps.station_id
LEFT JOIN pumps p ON pm.station_id = p.station_id AND pm.pump_id = p.pump_id
WHERE pm.enabled = TRUE
LOOP
-- Generate human-readable signal name
station_name_text := COALESCE(mapping_record.station_name, 'Unknown Station');
pump_name_text := COALESCE(mapping_record.pump_name, 'Unknown Pump');
signal_name_text := CONCAT(
station_name_text, ' ',
pump_name_text, ' ',
CASE mapping_record.data_type
WHEN 'setpoint' THEN 'Setpoint'
WHEN 'status' THEN 'Status'
WHEN 'control' THEN 'Control'
WHEN 'safety' THEN 'Safety'
WHEN 'alarm' THEN 'Alarm'
WHEN 'configuration' THEN 'Configuration'
ELSE INITCAP(mapping_record.data_type)
END
);
-- Generate tags array
tags_array := ARRAY[
-- Station tags
CASE
WHEN mapping_record.station_id LIKE '%main%' THEN 'station:main'
WHEN mapping_record.station_id LIKE '%backup%' THEN 'station:backup'
WHEN mapping_record.station_id LIKE '%control%' THEN 'station:control'
ELSE 'station:unknown'
END,
-- Equipment tags
CASE
WHEN mapping_record.pump_id LIKE '%primary%' THEN 'equipment:primary_pump'
WHEN mapping_record.pump_id LIKE '%backup%' THEN 'equipment:backup_pump'
WHEN mapping_record.pump_id LIKE '%sensor%' THEN 'equipment:sensor'
WHEN mapping_record.pump_id LIKE '%valve%' THEN 'equipment:valve'
WHEN mapping_record.pump_id LIKE '%controller%' THEN 'equipment:controller'
ELSE 'equipment:unknown'
END,
-- Data type tags
'data_type:' || mapping_record.data_type,
-- Protocol tags
'protocol:' || mapping_record.protocol_type
];
-- Generate signal ID (use existing mapping_id if it follows new pattern, otherwise create new)
IF mapping_record.mapping_id ~ '^[a-zA-Z0-9_-]+$' THEN
signal_id_text := mapping_record.mapping_id;
ELSE
signal_id_text := CONCAT(
REPLACE(LOWER(station_name_text), ' ', '_'), '_',
REPLACE(LOWER(pump_name_text), ' ', '_'), '_',
mapping_record.data_type, '_',
SUBSTRING(mapping_record.mapping_id, 1, 8)
);
END IF;
-- Insert into new table
INSERT INTO protocol_signals (
signal_id, signal_name, tags, protocol_type, protocol_address, db_source
) VALUES (
signal_id_text,
signal_name_text,
tags_array,
mapping_record.protocol_type,
mapping_record.protocol_address,
mapping_record.db_source
);
migrated_count := migrated_count + 1;
END LOOP;
RETURN migrated_count;
END;
$$ LANGUAGE plpgsql;
-- =============================================
-- STEP 3: Migration validation function
-- =============================================
CREATE OR REPLACE FUNCTION validate_migration()
RETURNS TABLE(
original_count INTEGER,
migrated_count INTEGER,
validation_status TEXT
) AS $$
BEGIN
-- Count original mappings
SELECT COUNT(*) INTO original_count FROM protocol_mappings WHERE enabled = TRUE;
-- Count migrated signals
SELECT COUNT(*) INTO migrated_count FROM protocol_signals;
-- Determine validation status
IF original_count = migrated_count THEN
validation_status := 'SUCCESS';
ELSIF migrated_count > 0 THEN
validation_status := 'PARTIAL_SUCCESS';
ELSE
validation_status := 'FAILED';
END IF;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql;
-- =============================================
-- STEP 4: Rollback function (for safety)
-- =============================================
CREATE OR REPLACE FUNCTION rollback_migration()
RETURNS VOID AS $$
BEGIN
-- Drop the new table if migration needs to be rolled back
DROP TABLE IF EXISTS protocol_signals;
-- Drop migration functions
DROP FUNCTION IF EXISTS migrate_protocol_mappings_to_signals();
DROP FUNCTION IF EXISTS validate_migration();
DROP FUNCTION IF EXISTS rollback_migration();
END;
$$ LANGUAGE plpgsql;
-- =============================================
-- STEP 5: Usage instructions
-- =============================================
COMMENT ON FUNCTION migrate_protocol_mappings_to_signals() IS 'Migrate existing protocol mappings to new simplified signals format';
COMMENT ON FUNCTION validate_migration() IS 'Validate that migration completed successfully';
COMMENT ON FUNCTION rollback_migration() IS 'Rollback migration by removing new tables and functions';
-- Example usage:
-- SELECT migrate_protocol_mappings_to_signals(); -- Run migration
-- SELECT * FROM validate_migration(); -- Validate results
-- SELECT rollback_migration(); -- Rollback if needed

View File

@ -939,6 +939,229 @@ async def delete_protocol_mapping(mapping_id: str):
# Protocol Discovery API Endpoints # Protocol Discovery API Endpoints
# Simplified Protocol Signals API Endpoints
@dashboard_router.get("/protocol-signals")
async def get_protocol_signals(
tags: Optional[str] = None,
protocol_type: Optional[str] = None,
signal_name_contains: Optional[str] = None,
enabled: Optional[bool] = True
):
"""Get protocol signals with simplified name + tags approach"""
try:
from .simplified_models import ProtocolSignalFilter, ProtocolType
from .simplified_configuration_manager import simplified_configuration_manager
# Parse tags from comma-separated string
tag_list = tags.split(",") if tags else None
# Convert protocol_type string to enum if provided
protocol_enum = None
if protocol_type:
try:
protocol_enum = ProtocolType(protocol_type)
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid protocol type: {protocol_type}")
# Create filter
filters = ProtocolSignalFilter(
tags=tag_list,
protocol_type=protocol_enum,
signal_name_contains=signal_name_contains,
enabled=enabled
)
signals = simplified_configuration_manager.get_protocol_signals(filters)
return {
"success": True,
"signals": [signal.dict() for signal in signals],
"count": len(signals)
}
except Exception as e:
logger.error(f"Error getting protocol signals: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get protocol signals: {str(e)}")
@dashboard_router.get("/protocol-signals/{signal_id}")
async def get_protocol_signal(signal_id: str):
"""Get a specific protocol signal by ID"""
try:
from .simplified_configuration_manager import simplified_configuration_manager
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
if not signal:
raise HTTPException(status_code=404, detail=f"Protocol signal {signal_id} not found")
return {
"success": True,
"signal": signal.dict()
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting protocol signal {signal_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get protocol signal: {str(e)}")
@dashboard_router.post("/protocol-signals")
async def create_protocol_signal(signal_data: dict):
"""Create a new protocol signal with simplified name + tags"""
try:
from .simplified_models import ProtocolSignalCreate, ProtocolType
from .simplified_configuration_manager import simplified_configuration_manager
# Convert protocol_type string to enum
if "protocol_type" not in signal_data:
raise HTTPException(status_code=400, detail="protocol_type is required")
try:
protocol_enum = ProtocolType(signal_data["protocol_type"])
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid protocol type: {signal_data['protocol_type']}")
# Create ProtocolSignalCreate object
signal_create = ProtocolSignalCreate(
signal_name=signal_data.get("signal_name"),
tags=signal_data.get("tags", []),
protocol_type=protocol_enum,
protocol_address=signal_data.get("protocol_address"),
db_source=signal_data.get("db_source"),
preprocessing_enabled=signal_data.get("preprocessing_enabled", False),
preprocessing_rules=signal_data.get("preprocessing_rules", []),
min_output_value=signal_data.get("min_output_value"),
max_output_value=signal_data.get("max_output_value"),
default_output_value=signal_data.get("default_output_value"),
modbus_config=signal_data.get("modbus_config"),
opcua_config=signal_data.get("opcua_config")
)
# Validate configuration
validation = simplified_configuration_manager.validate_signal_configuration(signal_create)
if not validation["valid"]:
return {
"success": False,
"message": "Configuration validation failed",
"errors": validation["errors"],
"warnings": validation["warnings"]
}
# Add the signal
success = simplified_configuration_manager.add_protocol_signal(signal_create)
if success:
# Get the created signal to return
signal_id = signal_create.generate_signal_id()
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
return {
"success": True,
"message": "Protocol signal created successfully",
"signal": signal.dict() if signal else None,
"warnings": validation["warnings"]
}
else:
raise HTTPException(status_code=400, detail="Failed to create protocol signal")
except ValidationError as e:
logger.error(f"Validation error creating protocol signal: {str(e)}")
raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
logger.error(f"Error creating protocol signal: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to create protocol signal: {str(e)}")
@dashboard_router.put("/protocol-signals/{signal_id}")
async def update_protocol_signal(signal_id: str, signal_data: dict):
"""Update an existing protocol signal"""
try:
from .simplified_models import ProtocolSignalUpdate, ProtocolType
from .simplified_configuration_manager import simplified_configuration_manager
# Convert protocol_type string to enum if provided
protocol_enum = None
if "protocol_type" in signal_data:
try:
protocol_enum = ProtocolType(signal_data["protocol_type"])
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid protocol type: {signal_data['protocol_type']}")
# Create ProtocolSignalUpdate object
update_data = ProtocolSignalUpdate(
signal_name=signal_data.get("signal_name"),
tags=signal_data.get("tags"),
protocol_type=protocol_enum,
protocol_address=signal_data.get("protocol_address"),
db_source=signal_data.get("db_source"),
preprocessing_enabled=signal_data.get("preprocessing_enabled"),
preprocessing_rules=signal_data.get("preprocessing_rules"),
min_output_value=signal_data.get("min_output_value"),
max_output_value=signal_data.get("max_output_value"),
default_output_value=signal_data.get("default_output_value"),
modbus_config=signal_data.get("modbus_config"),
opcua_config=signal_data.get("opcua_config"),
enabled=signal_data.get("enabled")
)
success = simplified_configuration_manager.update_protocol_signal(signal_id, update_data)
if success:
# Get the updated signal to return
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
return {
"success": True,
"message": "Protocol signal updated successfully",
"signal": signal.dict() if signal else None
}
else:
raise HTTPException(status_code=404, detail=f"Protocol signal {signal_id} not found")
except ValidationError as e:
logger.error(f"Validation error updating protocol signal: {str(e)}")
raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
except Exception as e:
logger.error(f"Error updating protocol signal {signal_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to update protocol signal: {str(e)}")
@dashboard_router.delete("/protocol-signals/{signal_id}")
async def delete_protocol_signal(signal_id: str):
"""Delete a protocol signal"""
try:
from .simplified_configuration_manager import simplified_configuration_manager
success = simplified_configuration_manager.delete_protocol_signal(signal_id)
if success:
return {
"success": True,
"message": f"Protocol signal {signal_id} deleted successfully"
}
else:
raise HTTPException(status_code=404, detail=f"Protocol signal {signal_id} not found")
except Exception as e:
logger.error(f"Error deleting protocol signal {signal_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to delete protocol signal: {str(e)}")
@dashboard_router.get("/protocol-signals/tags/all")
async def get_all_signal_tags():
"""Get all unique tags used across protocol signals"""
try:
from .simplified_configuration_manager import simplified_configuration_manager
all_tags = simplified_configuration_manager.get_all_tags()
return {
"success": True,
"tags": all_tags,
"count": len(all_tags)
}
except Exception as e:
logger.error(f"Error getting all signal tags: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get signal tags: {str(e)}")
# Tag-Based Metadata API Endpoints # Tag-Based Metadata API Endpoints
@dashboard_router.get("/metadata/summary") @dashboard_router.get("/metadata/summary")

View File

@ -0,0 +1,277 @@
"""
Simplified Configuration Manager
Manages protocol signals with human-readable names and tags
Replaces the complex ID-based system
"""
import logging
from typing import List, Optional, Dict, Any
from datetime import datetime
from .simplified_models import (
ProtocolSignal, ProtocolSignalCreate, ProtocolSignalUpdate,
ProtocolSignalFilter, ProtocolType
)
logger = logging.getLogger(__name__)
class SimplifiedConfigurationManager:
"""
Manages protocol signals with simplified name + tags approach
"""
def __init__(self, database_client=None):
self.database_client = database_client
self.signals: Dict[str, ProtocolSignal] = {}
logger.info("SimplifiedConfigurationManager initialized")
def add_protocol_signal(self, signal_create: ProtocolSignalCreate) -> bool:
"""
Add a new protocol signal
"""
try:
# Generate signal ID
signal_id = signal_create.generate_signal_id()
# Check if signal ID already exists
if signal_id in self.signals:
logger.warning(f"Signal ID {signal_id} already exists")
return False
# Create ProtocolSignal object
signal = ProtocolSignal(
signal_id=signal_id,
signal_name=signal_create.signal_name,
tags=signal_create.tags,
protocol_type=signal_create.protocol_type,
protocol_address=signal_create.protocol_address,
db_source=signal_create.db_source,
preprocessing_enabled=signal_create.preprocessing_enabled,
preprocessing_rules=signal_create.preprocessing_rules,
min_output_value=signal_create.min_output_value,
max_output_value=signal_create.max_output_value,
default_output_value=signal_create.default_output_value,
modbus_config=signal_create.modbus_config,
opcua_config=signal_create.opcua_config,
created_at=datetime.now().isoformat(),
updated_at=datetime.now().isoformat()
)
# Store in memory (in production, this would be in database)
self.signals[signal_id] = signal
logger.info(f"Added protocol signal: {signal_id} - {signal.signal_name}")
return True
except Exception as e:
logger.error(f"Error adding protocol signal: {str(e)}")
return False
def get_protocol_signals(self, filters: Optional[ProtocolSignalFilter] = None) -> List[ProtocolSignal]:
"""
Get protocol signals with optional filtering
"""
try:
signals = list(self.signals.values())
if not filters:
return signals
# Apply filters
filtered_signals = signals
# Filter by tags
if filters.tags:
filtered_signals = [
s for s in filtered_signals
if any(tag in s.tags for tag in filters.tags)
]
# Filter by protocol type
if filters.protocol_type:
filtered_signals = [
s for s in filtered_signals
if s.protocol_type == filters.protocol_type
]
# Filter by signal name
if filters.signal_name_contains:
filtered_signals = [
s for s in filtered_signals
if filters.signal_name_contains.lower() in s.signal_name.lower()
]
# Filter by enabled status
if filters.enabled is not None:
filtered_signals = [
s for s in filtered_signals
if s.enabled == filters.enabled
]
return filtered_signals
except Exception as e:
logger.error(f"Error getting protocol signals: {str(e)}")
return []
def get_protocol_signal(self, signal_id: str) -> Optional[ProtocolSignal]:
"""
Get a specific protocol signal by ID
"""
return self.signals.get(signal_id)
def update_protocol_signal(self, signal_id: str, update_data: ProtocolSignalUpdate) -> bool:
"""
Update an existing protocol signal
"""
try:
if signal_id not in self.signals:
logger.warning(f"Signal {signal_id} not found for update")
return False
signal = self.signals[signal_id]
# Update fields if provided
if update_data.signal_name is not None:
signal.signal_name = update_data.signal_name
if update_data.tags is not None:
signal.tags = update_data.tags
if update_data.protocol_type is not None:
signal.protocol_type = update_data.protocol_type
if update_data.protocol_address is not None:
signal.protocol_address = update_data.protocol_address
if update_data.db_source is not None:
signal.db_source = update_data.db_source
if update_data.preprocessing_enabled is not None:
signal.preprocessing_enabled = update_data.preprocessing_enabled
if update_data.preprocessing_rules is not None:
signal.preprocessing_rules = update_data.preprocessing_rules
if update_data.min_output_value is not None:
signal.min_output_value = update_data.min_output_value
if update_data.max_output_value is not None:
signal.max_output_value = update_data.max_output_value
if update_data.default_output_value is not None:
signal.default_output_value = update_data.default_output_value
if update_data.modbus_config is not None:
signal.modbus_config = update_data.modbus_config
if update_data.opcua_config is not None:
signal.opcua_config = update_data.opcua_config
if update_data.enabled is not None:
signal.enabled = update_data.enabled
# Update timestamp
signal.updated_at = datetime.now().isoformat()
logger.info(f"Updated protocol signal: {signal_id}")
return True
except Exception as e:
logger.error(f"Error updating protocol signal {signal_id}: {str(e)}")
return False
def delete_protocol_signal(self, signal_id: str) -> bool:
"""
Delete a protocol signal
"""
try:
if signal_id not in self.signals:
logger.warning(f"Signal {signal_id} not found for deletion")
return False
del self.signals[signal_id]
logger.info(f"Deleted protocol signal: {signal_id}")
return True
except Exception as e:
logger.error(f"Error deleting protocol signal {signal_id}: {str(e)}")
return False
def search_signals_by_tags(self, tags: List[str]) -> List[ProtocolSignal]:
"""
Search signals by tags (all tags must match)
"""
try:
return [
signal for signal in self.signals.values()
if all(tag in signal.tags for tag in tags)
]
except Exception as e:
logger.error(f"Error searching signals by tags: {str(e)}")
return []
def get_all_tags(self) -> List[str]:
"""
Get all unique tags used across all signals
"""
all_tags = set()
for signal in self.signals.values():
all_tags.update(signal.tags)
return sorted(list(all_tags))
def get_signals_by_protocol_type(self, protocol_type: ProtocolType) -> List[ProtocolSignal]:
"""
Get all signals for a specific protocol type
"""
return [
signal for signal in self.signals.values()
if signal.protocol_type == protocol_type
]
def validate_signal_configuration(self, signal_create: ProtocolSignalCreate) -> Dict[str, Any]:
"""
Validate signal configuration before creation
"""
validation_result = {
"valid": True,
"errors": [],
"warnings": []
}
try:
# Validate signal name
if not signal_create.signal_name or not signal_create.signal_name.strip():
validation_result["valid"] = False
validation_result["errors"].append("Signal name cannot be empty")
# Validate protocol address
if not signal_create.protocol_address:
validation_result["valid"] = False
validation_result["errors"].append("Protocol address cannot be empty")
# Validate database source
if not signal_create.db_source:
validation_result["valid"] = False
validation_result["errors"].append("Database source cannot be empty")
# Check for duplicate signal names
existing_names = [s.signal_name for s in self.signals.values()]
if signal_create.signal_name in existing_names:
validation_result["warnings"].append(
f"Signal name '{signal_create.signal_name}' already exists"
)
# Validate tags
if not signal_create.tags:
validation_result["warnings"].append("No tags provided - consider adding tags for better organization")
return validation_result
except Exception as e:
validation_result["valid"] = False
validation_result["errors"].append(f"Validation error: {str(e)}")
return validation_result
# Global instance for simplified configuration management
simplified_configuration_manager = SimplifiedConfigurationManager()

View File

@ -0,0 +1,195 @@
"""
Simplified Protocol Signal Models
Migration from complex ID system to simple signal names + tags
"""
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, validator
from enum import Enum
import uuid
import logging
logger = logging.getLogger(__name__)
class ProtocolType(str, Enum):
"""Supported protocol types"""
OPCUA = "opcua"
MODBUS_TCP = "modbus_tcp"
MODBUS_RTU = "modbus_rtu"
REST_API = "rest_api"
class ProtocolSignal(BaseModel):
"""
Simplified protocol signal with human-readable name and tags
Replaces the complex station_id/equipment_id/data_type_id system
"""
signal_id: str
signal_name: str
tags: List[str]
protocol_type: ProtocolType
protocol_address: str
db_source: str
# 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
# Metadata
created_at: Optional[str] = None
updated_at: Optional[str] = None
created_by: Optional[str] = None
enabled: bool = True
@validator('signal_id')
def validate_signal_id(cls, v):
"""Validate signal ID format"""
if not v.replace('_', '').replace('-', '').isalnum():
raise ValueError("Signal ID must be alphanumeric with underscores and hyphens")
return v
@validator('signal_name')
def validate_signal_name(cls, v):
"""Validate signal name is not empty"""
if not v or not v.strip():
raise ValueError("Signal name cannot be empty")
return v.strip()
@validator('tags')
def validate_tags(cls, v):
"""Validate tags format"""
if not isinstance(v, list):
raise ValueError("Tags must be a list")
# Remove empty tags and normalize
cleaned_tags = []
for tag in v:
if tag and isinstance(tag, str) and tag.strip():
cleaned_tags.append(tag.strip().lower())
return cleaned_tags
@validator('protocol_address')
def validate_protocol_address(cls, v, values):
"""Validate protocol address based on protocol type"""
if 'protocol_type' not in values:
return v
protocol_type = values['protocol_type']
if protocol_type == ProtocolType.MODBUS_TCP or protocol_type == ProtocolType.MODBUS_RTU:
# Modbus addresses should be numeric
if not v.isdigit():
raise ValueError(f"Modbus address must be numeric, got: {v}")
address = int(v)
if address < 0 or address > 65535:
raise ValueError(f"Modbus address must be between 0 and 65535, got: {address}")
elif protocol_type == ProtocolType.OPCUA:
# OPC UA addresses should follow NodeId format
if not v.startswith(('ns=', 'i=', 's=')):
raise ValueError(f"OPC UA address should start with ns=, i=, or s=, got: {v}")
elif protocol_type == ProtocolType.REST_API:
# REST API addresses should be URLs or paths
if not v.startswith('/'):
raise ValueError(f"REST API address should start with /, got: {v}")
return v
class ProtocolSignalCreate(BaseModel):
"""Model for creating new protocol signals"""
signal_name: str
tags: List[str]
protocol_type: ProtocolType
protocol_address: str
db_source: str
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
modbus_config: Optional[Dict[str, Any]] = None
opcua_config: Optional[Dict[str, Any]] = None
def generate_signal_id(self) -> str:
"""Generate a unique signal ID from the signal name"""
base_id = self.signal_name.lower().replace(' ', '_').replace('/', '_')
base_id = ''.join(c for c in base_id if c.isalnum() or c in ['_', '-'])
# Add random suffix to ensure uniqueness
random_suffix = uuid.uuid4().hex[:8]
return f"{base_id}_{random_suffix}"
class ProtocolSignalUpdate(BaseModel):
"""Model for updating existing protocol signals"""
signal_name: Optional[str] = None
tags: Optional[List[str]] = None
protocol_type: Optional[ProtocolType] = None
protocol_address: Optional[str] = None
db_source: Optional[str] = None
preprocessing_enabled: Optional[bool] = None
preprocessing_rules: Optional[List[Dict[str, Any]]] = None
min_output_value: Optional[float] = None
max_output_value: Optional[float] = None
default_output_value: Optional[float] = None
modbus_config: Optional[Dict[str, Any]] = None
opcua_config: Optional[Dict[str, Any]] = None
enabled: Optional[bool] = None
class ProtocolSignalFilter(BaseModel):
"""Model for filtering protocol signals"""
tags: Optional[List[str]] = None
protocol_type: Optional[ProtocolType] = None
signal_name_contains: Optional[str] = None
enabled: Optional[bool] = True
class SignalDiscoveryResult(BaseModel):
"""Model for discovery results that can be converted to protocol signals"""
device_name: str
protocol_type: ProtocolType
protocol_address: str
data_point: str
device_address: Optional[str] = None
device_port: Optional[int] = None
def to_protocol_signal_create(self) -> ProtocolSignalCreate:
"""Convert discovery result to protocol signal creation data"""
signal_name = f"{self.device_name} {self.data_point}"
# Generate meaningful tags from discovery data
tags = [
f"device:{self.device_name.lower().replace(' ', '_')}",
f"protocol:{self.protocol_type.value}",
f"data_point:{self.data_point.lower().replace(' ', '_')}"
]
if self.device_address:
tags.append(f"address:{self.device_address}")
return ProtocolSignalCreate(
signal_name=signal_name,
tags=tags,
protocol_type=self.protocol_type,
protocol_address=self.protocol_address,
db_source=f"measurements.{self.device_name.lower().replace(' ', '_')}_{self.data_point.lower().replace(' ', '_')}"
)
# Example usage:
# discovery_result = SignalDiscoveryResult(
# device_name="Water Pump Controller",
# protocol_type=ProtocolType.MODBUS_TCP,
# protocol_address="40001",
# data_point="Speed",
# device_address="192.168.1.100"
# )
#
# signal_create = discovery_result.to_protocol_signal_create()
# print(signal_create.signal_name) # "Water Pump Controller Speed"
# print(signal_create.tags) # ["device:water_pump_controller", "protocol:modbus_tcp", "data_point:speed", "address:192.168.1.100"]

View File

@ -0,0 +1,164 @@
"""
Simplified Protocol Signals HTML Template
"""
SIMPLIFIED_PROTOCOL_SIGNALS_HTML = """
<div id="protocol-mapping-tab" class="tab-content">
<h2>Protocol Signals Management</h2>
<div id="protocol-mapping-alerts"></div>
<!-- Simplified Protocol Signals Interface -->
<div class="config-section">
<h3>Protocol Signals</h3>
<p>Manage your industrial protocol signals with human-readable names and flexible tags</p>
<!-- Filter Controls -->
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 15px; margin-bottom: 20px;">
<div>
<label for="name-filter" style="display: block; margin-bottom: 5px; font-weight: bold;">Signal Name</label>
<input type="text" id="name-filter" placeholder="Filter by signal name..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div>
<label for="tag-filter" style="display: block; margin-bottom: 5px; font-weight: bold;">Tags</label>
<input type="text" id="tag-filter" placeholder="Filter by tags..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div>
<label for="protocol-filter" style="display: block; margin-bottom: 5px; font-weight: bold;">Protocol Type</label>
<select id="protocol-filter" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<option value="all">All Protocols</option>
<option value="modbus_tcp">Modbus TCP</option>
<option value="modbus_rtu">Modbus RTU</option>
<option value="opcua">OPC UA</option>
<option value="rest_api">REST API</option>
</select>
</div>
<div style="align-self: end;">
<button onclick="applyFilters()" style="background: #007acc; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;">Apply Filters</button>
</div>
</div>
<!-- Tag Cloud -->
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 20px;">
<h4 style="margin-bottom: 10px;">Popular Tags</h4>
<div id="tag-cloud">
<!-- Tags will be populated by JavaScript -->
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button onclick="loadProtocolSignals()">Refresh Signals</button>
<button onclick="showAddSignalModal()" style="background: #28a745;">Add New Signal</button>
<button onclick="exportProtocolSignals()">Export to CSV</button>
</div>
<!-- Signals Table -->
<div style="margin-top: 20px;">
<table style="width: 100%; border-collapse: collapse;" id="protocol-signals-table">
<thead>
<tr style="background: #f8f9fa;">
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Signal Name</th>
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Protocol Type</th>
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Tags</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;">Status</th>
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Actions</th>
</tr>
</thead>
<tbody id="protocol-signals-body">
<!-- Protocol signals will be populated by JavaScript -->
</tbody>
</table>
</div>
</div>
<!-- Protocol Discovery -->
<div class="config-section">
<h3>Protocol Discovery</h3>
<div id="discovery-notifications"></div>
<div class="discovery-controls">
<div class="action-buttons">
<button id="start-discovery-scan" class="btn-primary">
<i class="fas fa-search"></i> Start Discovery Scan
</button>
<button id="stop-discovery-scan" class="btn-secondary" disabled>
<i class="fas fa-stop"></i> Stop Scan
</button>
<button id="refresh-discovery-status" class="btn-outline">
<i class="fas fa-sync"></i> Refresh Status
</button>
</div>
<div id="discovery-status" style="margin-top: 15px;">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
Discovery service ready - Discovered devices will auto-populate signal forms
</div>
</div>
</div>
<div id="discovery-results" style="margin-top: 20px;">
<!-- Discovery results will be populated here -->
</div>
</div>
<!-- Add/Edit Signal Modal -->
<div id="signal-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" onclick="closeSignalModal()">&times;</span>
<h3 id="modal-title">Add Protocol Signal</h3>
<form id="signal-form">
<div class="form-group">
<label for="signal_name">Signal Name *</label>
<input type="text" id="signal_name" name="signal_name" required>
<small style="color: #666;">Human-readable name for this signal (e.g., "Main Pump Speed")</small>
</div>
<div class="form-group">
<label for="tags">Tags</label>
<input type="text" id="tags" name="tags" placeholder="equipment:pump, protocol:modbus_tcp, data_point:speed">
<small style="color: #666;">Comma-separated tags for categorization and filtering</small>
</div>
<div class="form-group">
<label for="protocol_type">Protocol Type *</label>
<select id="protocol_type" name="protocol_type" required onchange="updateProtocolFields()">
<option value="">Select Protocol Type</option>
<option value="modbus_tcp">Modbus TCP</option>
<option value="modbus_rtu">Modbus RTU</option>
<option value="opcua">OPC UA</option>
<option value="rest_api">REST API</option>
</select>
</div>
<div class="form-group">
<label for="protocol_address">Protocol Address *</label>
<input type="text" id="protocol_address" name="protocol_address" required>
<small id="protocol-address-help" style="color: #666;"></small>
</div>
<div class="form-group">
<label for="db_source">Database Source *</label>
<input type="text" id="db_source" name="db_source" required>
<small style="color: #666;">Database table and column name (e.g., measurements.pump_speed)</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="preprocessing_enabled" name="preprocessing_enabled">
Enable Signal Preprocessing
</label>
</div>
<div class="action-buttons">
<button type="button" onclick="validateSignal()">Validate</button>
<button type="submit" style="background: #28a745;">Save Signal</button>
<button type="button" onclick="closeSignalModal()" style="background: #dc3545;">Cancel</button>
</div>
</form>
</div>
</div>
</div>
"""

File diff suppressed because it is too large Load Diff

View File

@ -1,229 +1,157 @@
// Protocol Mapping Functions // Simplified Protocol Mapping Functions
// Uses human-readable signal names and tags instead of complex IDs
let currentProtocolFilter = 'all'; let currentProtocolFilter = 'all';
let editingMappingId = null; let editingSignalId = null;
let tagMetadata = { let allTags = new Set();
stations: [],
equipment: [],
dataTypes: []
};
// Tag Metadata Functions // Simplified Signal Management Functions
async function loadTagMetadata() { async function loadAllSignals() {
try { try {
// Load stations const response = await fetch('/api/v1/dashboard/protocol-signals');
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;
// Update active button
document.querySelectorAll('.protocol-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Reload mappings with filter
loadProtocolMappings();
}
async function loadProtocolMappings() {
try {
// Ensure tag metadata is loaded first
if (tagMetadata.stations.length === 0 || tagMetadata.dataTypes.length === 0) {
await loadTagMetadata();
}
const params = new URLSearchParams();
if (currentProtocolFilter !== 'all') {
params.append('protocol_type', currentProtocolFilter);
}
const response = await fetch(`/api/v1/dashboard/protocol-mappings?${params}`);
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
displayProtocolMappings(data.mappings); displaySignals(data.signals);
updateTagCloud(data.signals);
} else { } else {
showProtocolMappingAlert('Failed to load protocol mappings', 'error'); showSimplifiedAlert('Failed to load signals', 'error');
} }
} catch (error) { } catch (error) {
console.error('Error loading protocol mappings:', error); console.error('Error loading signals:', error);
showProtocolMappingAlert('Error loading protocol mappings', 'error'); showSimplifiedAlert('Error loading signals', 'error');
} }
} }
function displayProtocolMappings(mappings) { function displaySignals(signals) {
const tbody = document.getElementById('protocol-mappings-body'); const tbody = document.getElementById('protocol-signals-body');
tbody.innerHTML = ''; tbody.innerHTML = '';
if (mappings.length === 0) { if (signals.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px;">No protocol mappings found</td></tr>'; tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 20px;">No protocol signals found</td></tr>';
return; return;
} }
mappings.forEach(mapping => { signals.forEach(signal => {
// Look up human-readable names from tag metadata
const station = tagMetadata.stations.find(s => s.id === mapping.station_id);
const equipment = tagMetadata.equipment.find(e => e.id === mapping.equipment_id);
const dataType = tagMetadata.dataTypes.find(dt => dt.id === mapping.data_type_id);
const stationDisplay = station ? `${station.name} (${station.id})` : (mapping.station_id || '-');
const equipmentDisplay = equipment ? `${equipment.name} (${equipment.id})` : (mapping.equipment_id || '-');
const dataTypeDisplay = dataType ? `${dataType.name} (${dataType.id})` : (mapping.data_type_id || '-');
const row = document.createElement('tr'); const row = document.createElement('tr');
row.innerHTML = ` row.innerHTML = `
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.id}</td> <td style="padding: 10px; border: 1px solid #ddd;">${signal.signal_name}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.protocol_type}</td> <td style="padding: 10px; border: 1px solid #ddd;">${signal.protocol_type}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${stationDisplay}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${equipmentDisplay}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${dataTypeDisplay}</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;"> <td style="padding: 10px; border: 1px solid #ddd;">
<button onclick="editMapping('${mapping.id}')" style="background: #007acc; margin-right: 5px;">Edit</button> ${signal.tags.map(tag => `<span class="tag-badge">${tag}</span>`).join('')}
<button onclick="deleteMapping('${mapping.id}')" style="background: #dc3545;">Delete</button> </td>
<td style="padding: 10px; border: 1px solid #ddd;">${signal.protocol_address}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${signal.db_source}</td>
<td style="padding: 10px; border: 1px solid #ddd;">
<span class="status-badge ${signal.enabled ? 'enabled' : 'disabled'}">
${signal.enabled ? 'Enabled' : 'Disabled'}
</span>
</td>
<td style="padding: 10px; border: 1px solid #ddd;">
<button onclick="editSignal('${signal.signal_id}')" class="btn-edit">Edit</button>
<button onclick="deleteSignal('${signal.signal_id}')" class="btn-delete">Delete</button>
</td> </td>
`; `;
tbody.appendChild(row); tbody.appendChild(row);
}); });
} }
function showAddMappingModal() { function updateTagCloud(signals) {
editingMappingId = null; const tagCloud = document.getElementById('tag-cloud');
document.getElementById('modal-title').textContent = 'Add Protocol Mapping'; if (!tagCloud) return;
document.getElementById('mapping-form').reset();
document.getElementById('protocol_address_help').textContent = ''; // Collect all tags
document.getElementById('mapping-modal').style.display = 'block'; const tagCounts = {};
signals.forEach(signal => {
signal.tags.forEach(tag => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
});
// Create tag cloud
tagCloud.innerHTML = '';
Object.entries(tagCounts).forEach(([tag, count]) => {
const tagElement = document.createElement('span');
tagElement.className = 'tag-cloud-item';
tagElement.textContent = tag;
tagElement.title = `${count} signal(s)`;
tagElement.onclick = () => filterByTag(tag);
tagCloud.appendChild(tagElement);
});
} }
function showEditMappingModal(mapping) { function filterByTag(tag) {
editingMappingId = mapping.id; const filterInput = document.getElementById('tag-filter');
document.getElementById('modal-title').textContent = 'Edit Protocol Mapping'; if (filterInput) {
document.getElementById('mapping_id').value = mapping.id; filterInput.value = tag;
document.getElementById('protocol_type').value = mapping.protocol_type; applyFilters();
// 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; async function applyFilters() {
document.getElementById('db_source').value = mapping.db_source; const tagFilter = document.getElementById('tag-filter')?.value || '';
const protocolFilter = document.getElementById('protocol-filter')?.value || 'all';
const nameFilter = document.getElementById('name-filter')?.value || '';
const params = new URLSearchParams();
if (tagFilter) params.append('tags', tagFilter);
if (protocolFilter !== 'all') params.append('protocol_type', protocolFilter);
if (nameFilter) params.append('signal_name_contains', nameFilter);
try {
const response = await fetch(`/api/v1/dashboard/protocol-signals?${params}`);
const data = await response.json();
if (data.success) {
displaySignals(data.signals);
}
} catch (error) {
console.error('Error applying filters:', error);
}
}
// Modal Functions
function showAddSignalModal() {
editingSignalId = null;
document.getElementById('modal-title').textContent = 'Add Protocol Signal';
document.getElementById('signal-form').reset();
document.getElementById('protocol-address-help').textContent = '';
document.getElementById('signal-modal').style.display = 'block';
}
function showEditSignalModal(signal) {
editingSignalId = signal.signal_id;
document.getElementById('modal-title').textContent = 'Edit Protocol Signal';
// Populate form
document.getElementById('signal_name').value = signal.signal_name;
document.getElementById('tags').value = signal.tags.join(', ');
document.getElementById('protocol_type').value = signal.protocol_type;
document.getElementById('protocol_address').value = signal.protocol_address;
document.getElementById('db_source').value = signal.db_source;
document.getElementById('preprocessing_enabled').checked = signal.preprocessing_enabled || false;
updateProtocolFields(); updateProtocolFields();
document.getElementById('mapping-modal').style.display = 'block'; document.getElementById('signal-modal').style.display = 'block';
} }
function closeMappingModal() { function closeSignalModal() {
document.getElementById('mapping-modal').style.display = 'none'; document.getElementById('signal-modal').style.display = 'none';
editingMappingId = null; editingSignalId = null;
} }
function updateProtocolFields() { function updateProtocolFields() {
const protocolType = document.getElementById('protocol_type').value; const protocolType = document.getElementById('protocol_type').value;
const helpText = document.getElementById('protocol_address_help'); const helpText = document.getElementById('protocol-address-help');
switch (protocolType) { switch (protocolType) {
case 'modbus_tcp': case 'modbus_tcp':
case 'modbus_rtu':
helpText.textContent = 'Modbus address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)'; helpText.textContent = 'Modbus address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
break; break;
case 'opcua': case 'opcua':
helpText.textContent = 'OPC UA NodeId format: ns=2;s=MyVariable or ns=2;i=1234'; helpText.textContent = 'OPC UA NodeId format: ns=2;s=MyVariable or ns=2;i=1234';
break; break;
case 'modbus_rtu':
helpText.textContent = 'Modbus RTU address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
break;
case 'rest_api': case 'rest_api':
helpText.textContent = 'REST API endpoint format: /api/v1/data/endpoint'; helpText.textContent = 'REST API endpoint format: /api/v1/data/endpoint';
break; break;
@ -232,48 +160,22 @@ function updateProtocolFields() {
} }
} }
async function validateMapping() { // Form Submission
const formData = getMappingFormData(); async function saveSignal(event) {
try {
const response = await fetch(`/api/v1/dashboard/protocol-mappings/${editingMappingId || 'new'}/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await response.json();
if (data.success) {
if (data.valid) {
showProtocolMappingAlert('Mapping validation successful!', 'success');
} else {
showProtocolMappingAlert(`Validation failed: ${data.errors.join(', ')}`, 'error');
}
} else {
showProtocolMappingAlert('Validation error', 'error');
}
} catch (error) {
console.error('Error validating mapping:', error);
showProtocolMappingAlert('Error validating mapping', 'error');
}
}
async function saveMapping(event) {
event.preventDefault(); event.preventDefault();
const formData = getMappingFormData(); const formData = getSignalFormData();
try { try {
let response; let response;
if (editingMappingId) { if (editingSignalId) {
response = await fetch(`/api/v1/dashboard/protocol-mappings/${editingMappingId}`, { response = await fetch(`/api/v1/dashboard/protocol-signals/${editingSignalId}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData) body: JSON.stringify(formData)
}); });
} else { } else {
response = await fetch('/api/v1/dashboard/protocol-mappings', { response = await fetch('/api/v1/dashboard/protocol-signals', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData) body: JSON.stringify(formData)
@ -283,76 +185,151 @@ async function saveMapping(event) {
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
showProtocolMappingAlert(`Protocol mapping ${editingMappingId ? 'updated' : 'created'} successfully!`, 'success'); showSimplifiedAlert(`Protocol signal ${editingSignalId ? 'updated' : 'created'} successfully!`, 'success');
closeMappingModal(); closeSignalModal();
loadProtocolMappings(); loadAllSignals();
} else { } else {
showProtocolMappingAlert(`Failed to save mapping: ${data.detail || 'Unknown error'}`, 'error'); showSimplifiedAlert(`Failed to save signal: ${data.detail || 'Unknown error'}`, 'error');
} }
} catch (error) { } catch (error) {
console.error('Error saving mapping:', error); console.error('Error saving signal:', error);
showProtocolMappingAlert('Error saving mapping', 'error'); showSimplifiedAlert('Error saving signal', 'error');
} }
} }
function getMappingFormData() { function getSignalFormData() {
const tagsInput = document.getElementById('tags').value;
const tags = tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag);
return { return {
signal_name: document.getElementById('signal_name').value,
tags: tags,
protocol_type: document.getElementById('protocol_type').value, 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, protocol_address: document.getElementById('protocol_address').value,
db_source: document.getElementById('db_source').value db_source: document.getElementById('db_source').value,
preprocessing_enabled: document.getElementById('preprocessing_enabled').checked
}; };
} }
async function editMapping(mappingId) { // Signal Management
async function editSignal(signalId) {
try { try {
const response = await fetch(`/api/v1/dashboard/protocol-mappings?protocol_type=all`); const response = await fetch(`/api/v1/dashboard/protocol-signals/${signalId}`);
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
const mapping = data.mappings.find(m => m.id === mappingId); showEditSignalModal(data.signal);
if (mapping) {
showEditMappingModal(mapping);
} else { } else {
showProtocolMappingAlert('Mapping not found', 'error'); showSimplifiedAlert('Signal not found', 'error');
}
} else {
showProtocolMappingAlert('Failed to load mapping', 'error');
} }
} catch (error) { } catch (error) {
console.error('Error loading mapping:', error); console.error('Error loading signal:', error);
showProtocolMappingAlert('Error loading mapping', 'error'); showSimplifiedAlert('Error loading signal', 'error');
} }
} }
async function deleteMapping(mappingId) { async function deleteSignal(signalId) {
if (!confirm(`Are you sure you want to delete mapping ${mappingId}?`)) { if (!confirm('Are you sure you want to delete this signal?')) {
return; return;
} }
try { try {
const response = await fetch(`/api/v1/dashboard/protocol-mappings/${mappingId}`, { const response = await fetch(`/api/v1/dashboard/protocol-signals/${signalId}`, {
method: 'DELETE' method: 'DELETE'
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
showProtocolMappingAlert('Mapping deleted successfully!', 'success'); showSimplifiedAlert('Signal deleted successfully!', 'success');
loadProtocolMappings(); loadAllSignals();
} else { } else {
showProtocolMappingAlert(`Failed to delete mapping: ${data.detail || 'Unknown error'}`, 'error'); showSimplifiedAlert(`Failed to delete signal: ${data.detail || 'Unknown error'}`, 'error');
} }
} catch (error) { } catch (error) {
console.error('Error deleting mapping:', error); console.error('Error deleting signal:', error);
showProtocolMappingAlert('Error deleting mapping', 'error'); showSimplifiedAlert('Error deleting signal', 'error');
} }
} }
function showProtocolMappingAlert(message, type) { // Discovery Integration
const alertsDiv = document.getElementById('protocol-mapping-alerts'); function autoPopulateSignalForm(discoveryData) {
console.log('Auto-populating signal form with:', discoveryData);
// First, open the "Add New Signal" modal
showAddSignalModal();
// Wait for modal to be fully loaded and visible
const waitForModal = setInterval(() => {
const modal = document.getElementById('signal-modal');
const isModalVisible = modal && modal.style.display !== 'none';
if (isModalVisible) {
clearInterval(waitForModal);
populateModalFields(discoveryData);
}
}, 50);
// Timeout after 2 seconds
setTimeout(() => {
clearInterval(waitForModal);
const modal = document.getElementById('signal-modal');
if (modal && modal.style.display !== 'none') {
populateModalFields(discoveryData);
} else {
console.error('Modal did not open within timeout period');
showSimplifiedAlert('Could not open signal form. Please try opening it manually.', 'error');
}
}, 2000);
}
function populateModalFields(discoveryData) {
console.log('Populating modal fields with:', discoveryData);
// Populate signal name
const signalNameField = document.getElementById('signal_name');
if (signalNameField && discoveryData.signal_name) {
signalNameField.value = discoveryData.signal_name;
console.log('✓ Set signal_name to:', discoveryData.signal_name);
}
// Populate tags
const tagsField = document.getElementById('tags');
if (tagsField && discoveryData.tags) {
tagsField.value = discoveryData.tags.join(', ');
console.log('✓ Set tags to:', discoveryData.tags);
}
// Populate protocol type
const protocolTypeField = document.getElementById('protocol_type');
if (protocolTypeField && discoveryData.protocol_type) {
protocolTypeField.value = discoveryData.protocol_type;
console.log('✓ Set protocol_type to:', discoveryData.protocol_type);
// Trigger protocol field updates
protocolTypeField.dispatchEvent(new Event('change'));
}
// Populate protocol address
const protocolAddressField = document.getElementById('protocol_address');
if (protocolAddressField && discoveryData.protocol_address) {
protocolAddressField.value = discoveryData.protocol_address;
console.log('✓ Set protocol_address to:', discoveryData.protocol_address);
}
// Populate database source
const dbSourceField = document.getElementById('db_source');
if (dbSourceField && discoveryData.db_source) {
dbSourceField.value = discoveryData.db_source;
console.log('✓ Set db_source to:', discoveryData.db_source);
}
// Show success message
showSimplifiedAlert(`Signal form populated with discovery data. Please review and save.`, 'success');
}
// Utility Functions
function showSimplifiedAlert(message, type = 'info') {
const alertsDiv = document.getElementById('simplified-alerts');
const alertDiv = document.createElement('div'); const alertDiv = document.createElement('div');
alertDiv.className = `alert ${type === 'error' ? 'error' : 'success'}`; alertDiv.className = `alert ${type === 'error' ? 'error' : 'success'}`;
alertDiv.textContent = message; alertDiv.textContent = message;
@ -360,57 +337,21 @@ function showProtocolMappingAlert(message, type) {
alertsDiv.innerHTML = ''; alertsDiv.innerHTML = '';
alertsDiv.appendChild(alertDiv); alertsDiv.appendChild(alertDiv);
// Auto-remove after 5 seconds
setTimeout(() => { setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove(); alertDiv.remove();
}
}, 5000); }, 5000);
} }
async function exportProtocolMappings() { // Initialize
try {
const response = await fetch('/api/v1/dashboard/protocol-mappings?protocol_type=all');
const data = await response.json();
if (data.success) {
const csvContent = convertToCSV(data.mappings);
downloadCSV(csvContent, 'protocol_mappings.csv');
} else {
showProtocolMappingAlert('Failed to export mappings', 'error');
}
} catch (error) {
console.error('Error exporting mappings:', error);
showProtocolMappingAlert('Error exporting mappings', 'error');
}
}
function convertToCSV(mappings) {
const headers = ['ID', 'Protocol', 'Station', 'Pump', 'Data Type', 'Protocol Address', 'Database Source'];
const rows = mappings.map(mapping => [
mapping.id,
mapping.protocol_type,
mapping.station_id || '',
mapping.pump_id || '',
mapping.data_type,
mapping.protocol_address,
mapping.db_source
]);
return [headers, ...rows].map(row => row.map(field => `"${field}"`).join(',')).join('\n');
}
function downloadCSV(content, filename) {
const blob = new Blob([content], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
}
// Initialize form submission handler
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const mappingForm = document.getElementById('mapping-form'); const signalForm = document.getElementById('signal-form');
if (mappingForm) { if (signalForm) {
mappingForm.addEventListener('submit', saveMapping); signalForm.addEventListener('submit', saveSignal);
} }
// Load initial data
loadAllSignals();
}); });

View File

@ -0,0 +1,352 @@
// Simplified Discovery Integration
// Updated for simplified signal names + tags architecture
class SimplifiedProtocolDiscovery {
constructor() {
this.currentScanId = 'simplified-scan-123';
this.isScanning = false;
}
init() {
this.bindDiscoveryEvents();
}
bindDiscoveryEvents() {
// Auto-fill signal form from discovery
document.addEventListener('click', (e) => {
if (e.target.classList.contains('use-discovered-endpoint')) {
this.useDiscoveredEndpoint(e.target.dataset.endpointId);
}
});
}
async useDiscoveredEndpoint(endpointId) {
console.log('Using discovered endpoint:', endpointId);
// Mock endpoint data (in real implementation, this would come from discovery service)
const endpoints = {
'device_001': {
device_id: 'device_001',
protocol_type: 'modbus_tcp',
device_name: 'Water Pump Controller',
address: '192.168.1.100',
port: 502,
data_point: 'Speed',
protocol_address: '40001'
},
'device_002': {
device_id: 'device_002',
protocol_type: 'opcua',
device_name: 'Temperature Sensor',
address: '192.168.1.101',
port: 4840,
data_point: 'Temperature',
protocol_address: 'ns=2;s=Temperature'
},
'device_003': {
device_id: 'device_003',
protocol_type: 'modbus_tcp',
device_name: 'Pressure Transmitter',
address: '192.168.1.102',
port: 502,
data_point: 'Pressure',
protocol_address: '30001'
}
};
const endpoint = endpoints[endpointId];
if (!endpoint) {
this.showNotification(`Endpoint ${endpointId} not found`, 'error');
return;
}
// Convert to simplified signal format
const signalData = this.convertEndpointToSignal(endpoint);
// Auto-populate the signal form
this.autoPopulateSignalForm(signalData);
this.showNotification(`Endpoint ${endpoint.device_name} selected for signal creation`, 'success');
}
convertEndpointToSignal(endpoint) {
// Generate human-readable signal name
const signalName = `${endpoint.device_name} ${endpoint.data_point}`;
// Generate meaningful tags
const tags = [
`device:${endpoint.device_name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`,
`protocol:${endpoint.protocol_type}`,
`data_point:${endpoint.data_point.toLowerCase().replace(/[^a-z0-9]/g, '_')}`,
'discovered:true'
];
// Add device-specific tags
if (endpoint.device_name.toLowerCase().includes('pump')) {
tags.push('equipment:pump');
}
if (endpoint.device_name.toLowerCase().includes('sensor')) {
tags.push('equipment:sensor');
}
if (endpoint.device_name.toLowerCase().includes('controller')) {
tags.push('equipment:controller');
}
// Add protocol-specific tags
if (endpoint.protocol_type === 'modbus_tcp') {
tags.push('interface:modbus');
} else if (endpoint.protocol_type === 'opcua') {
tags.push('interface:opcua');
}
// Generate database source
const dbSource = `measurements.${endpoint.device_name.toLowerCase().replace(/[^a-z0-9]/g, '_')}_${endpoint.data_point.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
return {
signal_name: signalName,
tags: tags,
protocol_type: endpoint.protocol_type,
protocol_address: endpoint.protocol_address,
db_source: dbSource
};
}
autoPopulateSignalForm(signalData) {
console.log('Auto-populating signal form with:', signalData);
// Use the simplified protocol mapping function
if (typeof autoPopulateSignalForm === 'function') {
autoPopulateSignalForm(signalData);
} else {
console.error('Simplified protocol mapping functions not loaded');
this.showNotification('Protocol mapping system not available', 'error');
}
}
// Advanced discovery features
async discoverAndSuggestSignals(networkRange = '192.168.1.0/24') {
console.log(`Starting discovery scan on ${networkRange}`);
this.isScanning = true;
try {
// Mock discovery results
const discoveredEndpoints = await this.mockDiscoveryScan(networkRange);
// Convert to suggested signals
const suggestedSignals = discoveredEndpoints.map(endpoint =>
this.convertEndpointToSignal(endpoint)
);
this.displayDiscoveryResults(suggestedSignals);
this.isScanning = false;
return suggestedSignals;
} catch (error) {
console.error('Discovery scan failed:', error);
this.showNotification('Discovery scan failed', 'error');
this.isScanning = false;
return [];
}
}
async mockDiscoveryScan(networkRange) {
// Simulate network discovery delay
await new Promise(resolve => setTimeout(resolve, 2000));
// Return mock discovered endpoints
return [
{
device_id: 'discovered_001',
protocol_type: 'modbus_tcp',
device_name: 'Booster Pump',
address: '192.168.1.110',
port: 502,
data_point: 'Flow Rate',
protocol_address: '30002'
},
{
device_id: 'discovered_002',
protocol_type: 'modbus_tcp',
device_name: 'Level Sensor',
address: '192.168.1.111',
port: 502,
data_point: 'Tank Level',
protocol_address: '30003'
},
{
device_id: 'discovered_003',
protocol_type: 'opcua',
device_name: 'PLC Controller',
address: '192.168.1.112',
port: 4840,
data_point: 'System Status',
protocol_address: 'ns=2;s=SystemStatus'
}
];
}
displayDiscoveryResults(suggestedSignals) {
const resultsContainer = document.getElementById('discovery-results');
if (!resultsContainer) return;
resultsContainer.innerHTML = '<h3>Discovery Results</h3>';
suggestedSignals.forEach((signal, index) => {
const signalCard = document.createElement('div');
signalCard.className = 'discovery-result-card';
signalCard.innerHTML = `
<div class="signal-info">
<strong>${signal.signal_name}</strong>
<div class="signal-tags">
${signal.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
<div class="signal-details">
<span>Protocol: ${signal.protocol_type}</span>
<span>Address: ${signal.protocol_address}</span>
</div>
</div>
<button class="use-signal-btn" data-signal-index="${index}">
Use This Signal
</button>
`;
resultsContainer.appendChild(signalCard);
});
// Add event listeners for use buttons
resultsContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('use-signal-btn')) {
const signalIndex = parseInt(e.target.dataset.signalIndex);
const signal = suggestedSignals[signalIndex];
this.autoPopulateSignalForm(signal);
}
});
}
// Tag-based signal search
async searchSignalsByTags(tags) {
try {
const params = new URLSearchParams();
tags.forEach(tag => params.append('tags', tag));
const response = await fetch(`/api/v1/dashboard/protocol-signals?${params}`);
const data = await response.json();
if (data.success) {
return data.signals;
} else {
console.error('Failed to search signals by tags:', data.detail);
return [];
}
} catch (error) {
console.error('Error searching signals by tags:', error);
return [];
}
}
// Signal name suggestions based on device type
generateSignalNameSuggestions(deviceName, dataPoint) {
const baseName = `${deviceName} ${dataPoint}`;
const suggestions = [
baseName,
`${dataPoint} of ${deviceName}`,
`${deviceName} ${dataPoint} Reading`,
`${dataPoint} Measurement - ${deviceName}`
];
// Add context-specific suggestions
if (dataPoint.toLowerCase().includes('speed')) {
suggestions.push(`${deviceName} Motor Speed`);
suggestions.push(`${deviceName} RPM`);
}
if (dataPoint.toLowerCase().includes('temperature')) {
suggestions.push(`${deviceName} Temperature`);
suggestions.push(`Temperature at ${deviceName}`);
}
if (dataPoint.toLowerCase().includes('pressure')) {
suggestions.push(`${deviceName} Pressure`);
suggestions.push(`Pressure Reading - ${deviceName}`);
}
return suggestions;
}
// Tag suggestions based on device and protocol
generateTagSuggestions(deviceName, protocolType, dataPoint) {
const suggestions = new Set();
// Device type tags
if (deviceName.toLowerCase().includes('pump')) {
suggestions.add('equipment:pump');
suggestions.add('fluid:water');
}
if (deviceName.toLowerCase().includes('sensor')) {
suggestions.add('equipment:sensor');
suggestions.add('type:measurement');
}
if (deviceName.toLowerCase().includes('controller')) {
suggestions.add('equipment:controller');
suggestions.add('type:control');
}
// Protocol tags
suggestions.add(`protocol:${protocolType}`);
if (protocolType === 'modbus_tcp' || protocolType === 'modbus_rtu') {
suggestions.add('interface:modbus');
} else if (protocolType === 'opcua') {
suggestions.add('interface:opcua');
}
// Data point tags
suggestions.add(`data_point:${dataPoint.toLowerCase().replace(/[^a-z0-9]/g, '_')}`);
if (dataPoint.toLowerCase().includes('speed')) {
suggestions.add('unit:rpm');
suggestions.add('type:setpoint');
}
if (dataPoint.toLowerCase().includes('temperature')) {
suggestions.add('unit:celsius');
suggestions.add('type:measurement');
}
if (dataPoint.toLowerCase().includes('pressure')) {
suggestions.add('unit:psi');
suggestions.add('type:measurement');
}
if (dataPoint.toLowerCase().includes('status')) {
suggestions.add('type:status');
suggestions.add('format:boolean');
}
// Discovery tag
suggestions.add('discovered:true');
return Array.from(suggestions);
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `discovery-notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
}
// Global instance
const simplifiedDiscovery = new SimplifiedProtocolDiscovery();
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
simplifiedDiscovery.init();
});

View File

@ -0,0 +1,357 @@
// Simplified Protocol Mapping Functions
// Uses human-readable signal names and tags instead of complex IDs
let currentProtocolFilter = 'all';
let editingSignalId = null;
let allTags = new Set();
// Simplified Signal Management Functions
async function loadAllSignals() {
try {
const response = await fetch('/api/v1/dashboard/protocol-signals');
const data = await response.json();
if (data.success) {
displaySignals(data.signals);
updateTagCloud(data.signals);
} else {
showSimplifiedAlert('Failed to load signals', 'error');
}
} catch (error) {
console.error('Error loading signals:', error);
showSimplifiedAlert('Error loading signals', 'error');
}
}
function displaySignals(signals) {
const tbody = document.getElementById('protocol-signals-body');
tbody.innerHTML = '';
if (signals.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 20px;">No protocol signals found</td></tr>';
return;
}
signals.forEach(signal => {
const row = document.createElement('tr');
row.innerHTML = `
<td style="padding: 10px; border: 1px solid #ddd;">${signal.signal_name}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${signal.protocol_type}</td>
<td style="padding: 10px; border: 1px solid #ddd;">
${signal.tags.map(tag => `<span class="tag-badge">${tag}</span>`).join('')}
</td>
<td style="padding: 10px; border: 1px solid #ddd;">${signal.protocol_address}</td>
<td style="padding: 10px; border: 1px solid #ddd;">${signal.db_source}</td>
<td style="padding: 10px; border: 1px solid #ddd;">
<span class="status-badge ${signal.enabled ? 'enabled' : 'disabled'}">
${signal.enabled ? 'Enabled' : 'Disabled'}
</span>
</td>
<td style="padding: 10px; border: 1px solid #ddd;">
<button onclick="editSignal('${signal.signal_id}')" class="btn-edit">Edit</button>
<button onclick="deleteSignal('${signal.signal_id}')" class="btn-delete">Delete</button>
</td>
`;
tbody.appendChild(row);
});
}
function updateTagCloud(signals) {
const tagCloud = document.getElementById('tag-cloud');
if (!tagCloud) return;
// Collect all tags
const tagCounts = {};
signals.forEach(signal => {
signal.tags.forEach(tag => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
});
// Create tag cloud
tagCloud.innerHTML = '';
Object.entries(tagCounts).forEach(([tag, count]) => {
const tagElement = document.createElement('span');
tagElement.className = 'tag-cloud-item';
tagElement.textContent = tag;
tagElement.title = `${count} signal(s)`;
tagElement.onclick = () => filterByTag(tag);
tagCloud.appendChild(tagElement);
});
}
function filterByTag(tag) {
const filterInput = document.getElementById('tag-filter');
if (filterInput) {
filterInput.value = tag;
applyFilters();
}
}
async function applyFilters() {
const tagFilter = document.getElementById('tag-filter')?.value || '';
const protocolFilter = document.getElementById('protocol-filter')?.value || 'all';
const nameFilter = document.getElementById('name-filter')?.value || '';
const params = new URLSearchParams();
if (tagFilter) params.append('tags', tagFilter);
if (protocolFilter !== 'all') params.append('protocol_type', protocolFilter);
if (nameFilter) params.append('signal_name_contains', nameFilter);
try {
const response = await fetch(`/api/v1/dashboard/protocol-signals?${params}`);
const data = await response.json();
if (data.success) {
displaySignals(data.signals);
}
} catch (error) {
console.error('Error applying filters:', error);
}
}
// Modal Functions
function showAddSignalModal() {
editingSignalId = null;
document.getElementById('modal-title').textContent = 'Add Protocol Signal';
document.getElementById('signal-form').reset();
document.getElementById('protocol-address-help').textContent = '';
document.getElementById('signal-modal').style.display = 'block';
}
function showEditSignalModal(signal) {
editingSignalId = signal.signal_id;
document.getElementById('modal-title').textContent = 'Edit Protocol Signal';
// Populate form
document.getElementById('signal_name').value = signal.signal_name;
document.getElementById('tags').value = signal.tags.join(', ');
document.getElementById('protocol_type').value = signal.protocol_type;
document.getElementById('protocol_address').value = signal.protocol_address;
document.getElementById('db_source').value = signal.db_source;
document.getElementById('preprocessing_enabled').checked = signal.preprocessing_enabled || false;
updateProtocolFields();
document.getElementById('signal-modal').style.display = 'block';
}
function closeSignalModal() {
document.getElementById('signal-modal').style.display = 'none';
editingSignalId = null;
}
function updateProtocolFields() {
const protocolType = document.getElementById('protocol_type').value;
const helpText = document.getElementById('protocol-address-help');
switch (protocolType) {
case 'modbus_tcp':
case 'modbus_rtu':
helpText.textContent = 'Modbus address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
break;
case 'opcua':
helpText.textContent = 'OPC UA NodeId format: ns=2;s=MyVariable or ns=2;i=1234';
break;
case 'rest_api':
helpText.textContent = 'REST API endpoint format: /api/v1/data/endpoint';
break;
default:
helpText.textContent = '';
}
}
// Form Submission
async function saveSignal(event) {
event.preventDefault();
const formData = getSignalFormData();
try {
let response;
if (editingSignalId) {
response = await fetch(`/api/v1/dashboard/protocol-signals/${editingSignalId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
} else {
response = await fetch('/api/v1/dashboard/protocol-signals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
}
const data = await response.json();
if (data.success) {
showSimplifiedAlert(`Protocol signal ${editingSignalId ? 'updated' : 'created'} successfully!`, 'success');
closeSignalModal();
loadAllSignals();
} else {
showSimplifiedAlert(`Failed to save signal: ${data.detail || 'Unknown error'}`, 'error');
}
} catch (error) {
console.error('Error saving signal:', error);
showSimplifiedAlert('Error saving signal', 'error');
}
}
function getSignalFormData() {
const tagsInput = document.getElementById('tags').value;
const tags = tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag);
return {
signal_name: document.getElementById('signal_name').value,
tags: tags,
protocol_type: document.getElementById('protocol_type').value,
protocol_address: document.getElementById('protocol_address').value,
db_source: document.getElementById('db_source').value,
preprocessing_enabled: document.getElementById('preprocessing_enabled').checked
};
}
// Signal Management
async function editSignal(signalId) {
try {
const response = await fetch(`/api/v1/dashboard/protocol-signals/${signalId}`);
const data = await response.json();
if (data.success) {
showEditSignalModal(data.signal);
} else {
showSimplifiedAlert('Signal not found', 'error');
}
} catch (error) {
console.error('Error loading signal:', error);
showSimplifiedAlert('Error loading signal', 'error');
}
}
async function deleteSignal(signalId) {
if (!confirm('Are you sure you want to delete this signal?')) {
return;
}
try {
const response = await fetch(`/api/v1/dashboard/protocol-signals/${signalId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showSimplifiedAlert('Signal deleted successfully!', 'success');
loadAllSignals();
} else {
showSimplifiedAlert(`Failed to delete signal: ${data.detail || 'Unknown error'}`, 'error');
}
} catch (error) {
console.error('Error deleting signal:', error);
showSimplifiedAlert('Error deleting signal', 'error');
}
}
// Discovery Integration
function autoPopulateSignalForm(discoveryData) {
console.log('Auto-populating signal form with:', discoveryData);
// First, open the "Add New Signal" modal
showAddSignalModal();
// Wait for modal to be fully loaded and visible
const waitForModal = setInterval(() => {
const modal = document.getElementById('signal-modal');
const isModalVisible = modal && modal.style.display !== 'none';
if (isModalVisible) {
clearInterval(waitForModal);
populateModalFields(discoveryData);
}
}, 50);
// Timeout after 2 seconds
setTimeout(() => {
clearInterval(waitForModal);
const modal = document.getElementById('signal-modal');
if (modal && modal.style.display !== 'none') {
populateModalFields(discoveryData);
} else {
console.error('Modal did not open within timeout period');
showSimplifiedAlert('Could not open signal form. Please try opening it manually.', 'error');
}
}, 2000);
}
function populateModalFields(discoveryData) {
console.log('Populating modal fields with:', discoveryData);
// Populate signal name
const signalNameField = document.getElementById('signal_name');
if (signalNameField && discoveryData.signal_name) {
signalNameField.value = discoveryData.signal_name;
console.log('✓ Set signal_name to:', discoveryData.signal_name);
}
// Populate tags
const tagsField = document.getElementById('tags');
if (tagsField && discoveryData.tags) {
tagsField.value = discoveryData.tags.join(', ');
console.log('✓ Set tags to:', discoveryData.tags);
}
// Populate protocol type
const protocolTypeField = document.getElementById('protocol_type');
if (protocolTypeField && discoveryData.protocol_type) {
protocolTypeField.value = discoveryData.protocol_type;
console.log('✓ Set protocol_type to:', discoveryData.protocol_type);
// Trigger protocol field updates
protocolTypeField.dispatchEvent(new Event('change'));
}
// Populate protocol address
const protocolAddressField = document.getElementById('protocol_address');
if (protocolAddressField && discoveryData.protocol_address) {
protocolAddressField.value = discoveryData.protocol_address;
console.log('✓ Set protocol_address to:', discoveryData.protocol_address);
}
// Populate database source
const dbSourceField = document.getElementById('db_source');
if (dbSourceField && discoveryData.db_source) {
dbSourceField.value = discoveryData.db_source;
console.log('✓ Set db_source to:', discoveryData.db_source);
}
// Show success message
showSimplifiedAlert(`Signal form populated with discovery data. Please review and save.`, 'success');
}
// Utility Functions
function showSimplifiedAlert(message, type = 'info') {
const alertsDiv = document.getElementById('simplified-alerts');
const alertDiv = document.createElement('div');
alertDiv.className = `alert ${type === 'error' ? 'error' : 'success'}`;
alertDiv.textContent = message;
alertsDiv.innerHTML = '';
alertsDiv.appendChild(alertDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
const signalForm = document.getElementById('signal-form');
if (signalForm) {
signalForm.addEventListener('submit', saveSignal);
}
// Load initial data
loadAllSignals();
});

View File

@ -0,0 +1,361 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px 0;
border-radius: 10px;
margin-bottom: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.controls {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.filter-section {
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: 15px;
align-items: end;
}
.filter-group {
display: flex;
flex-direction: column;
}
.filter-group label {
font-weight: 600;
margin-bottom: 5px;
color: #555;
}
.filter-group input, .filter-group select {
padding: 10px;
border: 2px solid #e1e5e9;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.filter-group input:focus, .filter-group select:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.tag-cloud {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.tag-cloud h3 {
margin-bottom: 15px;
color: #333;
}
.tag-cloud-item {
display: inline-block;
background: #e9ecef;
padding: 5px 12px;
margin: 5px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s;
}
.tag-cloud-item:hover {
background: #667eea;
color: white;
transform: scale(1.05);
}
.signals-table {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.table-header {
background: #f8f9fa;
padding: 20px;
border-bottom: 1px solid #e1e5e9;
display: flex;
justify-content: space-between;
align-items: center;
}
.table-header h3 {
color: #333;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #e1e5e9;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #555;
}
tr:hover {
background: #f8f9fa;
}
.tag-badge {
display: inline-block;
background: #667eea;
color: white;
padding: 3px 8px;
margin: 2px;
border-radius: 12px;
font-size: 11px;
}
.status-badge {
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
font-weight: 600;
}
.status-badge.enabled {
background: #d4edda;
color: #155724;
}
.status-badge.disabled {
background: #f8d7da;
color: #721c24;
}
.btn-edit, .btn-delete {
padding: 6px 12px;
margin: 0 2px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn-edit {
background: #28a745;
color: white;
}
.btn-delete {
background: #dc3545;
color: white;
}
.btn-edit:hover {
background: #218838;
}
.btn-delete:hover {
background: #c82333;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 30px;
border-radius: 10px;
width: 90%;
max-width: 600px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e1e5e9;
}
.modal-header h2 {
color: #333;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: #333;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #555;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 10px;
border: 2px solid #e1e5e9;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.form-help {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 25px;
}
.alert {
padding: 15px;
margin: 20px 0;
border-radius: 6px;
font-weight: 500;
}
.alert.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.empty-state {
text-align: center;
padding: 50px 20px;
color: #6c757d;
}
.empty-state h3 {
margin-bottom: 10px;
}
@media (max-width: 768px) {
.filter-section {
grid-template-columns: 1fr;
}
.table-header {
flex-direction: column;
gap: 15px;
}
table {
font-size: 14px;
}
th, td {
padding: 10px;
}
}

View File

@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calejo Control - Protocol Signals</title>
<link rel="stylesheet" href="/static/simplified_styles.css">
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>Protocol Signals</h1>
<p>Manage your industrial protocol signals with human-readable names and flexible tags</p>
</div>
<!-- Alerts -->
<div id="simplified-alerts"></div>
<!-- Controls -->
<div class="controls">
<div class="filter-section">
<div class="filter-group">
<label for="name-filter">Signal Name</label>
<input type="text" id="name-filter" placeholder="Filter by signal name...">
</div>
<div class="filter-group">
<label for="tag-filter">Tags</label>
<input type="text" id="tag-filter" placeholder="Filter by tags...">
</div>
<div class="filter-group">
<label for="protocol-filter">Protocol Type</label>
<select id="protocol-filter">
<option value="all">All Protocols</option>
<option value="modbus_tcp">Modbus TCP</option>
<option value="modbus_rtu">Modbus RTU</option>
<option value="opcua">OPC UA</option>
<option value="rest_api">REST API</option>
</select>
</div>
<button class="btn btn-primary" onclick="applyFilters()">Apply Filters</button>
</div>
</div>
<!-- Tag Cloud -->
<div class="tag-cloud">
<h3>Popular Tags</h3>
<div id="tag-cloud">
<!-- Tags will be populated by JavaScript -->
</div>
</div>
<!-- Signals Table -->
<div class="signals-table">
<div class="table-header">
<h3>Protocol Signals</h3>
<button class="btn btn-primary" onclick="showAddSignalModal()">Add New Signal</button>
</div>
<table>
<thead>
<tr>
<th>Signal Name</th>
<th>Protocol Type</th>
<th>Tags</th>
<th>Protocol Address</th>
<th>Database Source</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="protocol-signals-body">
<!-- Signals will be populated by JavaScript -->
</tbody>
</table>
</div>
</div>
<!-- Add/Edit Signal Modal -->
<div id="signal-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Add Protocol Signal</h2>
<span class="close" onclick="closeSignalModal()">&times;</span>
</div>
<form id="signal-form">
<div class="form-group">
<label for="signal_name">Signal Name *</label>
<input type="text" id="signal_name" name="signal_name" required>
<div class="form-help">Human-readable name for this signal (e.g., "Main Pump Speed")</div>
</div>
<div class="form-group">
<label for="tags">Tags</label>
<input type="text" id="tags" name="tags" placeholder="equipment:pump, protocol:modbus_tcp, data_point:speed">
<div class="form-help">Comma-separated tags for categorization and filtering</div>
</div>
<div class="form-group">
<label for="protocol_type">Protocol Type *</label>
<select id="protocol_type" name="protocol_type" required onchange="updateProtocolFields()">
<option value="">Select Protocol Type</option>
<option value="modbus_tcp">Modbus TCP</option>
<option value="modbus_rtu">Modbus RTU</option>
<option value="opcua">OPC UA</option>
<option value="rest_api">REST API</option>
</select>
</div>
<div class="form-group">
<label for="protocol_address">Protocol Address *</label>
<input type="text" id="protocol_address" name="protocol_address" required>
<div class="form-help" id="protocol-address-help"></div>
</div>
<div class="form-group">
<label for="db_source">Database Source *</label>
<input type="text" id="db_source" name="db_source" required>
<div class="form-help">Database table and column name (e.g., measurements.pump_speed)</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="preprocessing_enabled" name="preprocessing_enabled">
Enable Signal Preprocessing
</label>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeSignalModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Signal</button>
</div>
</form>
</div>
</div>
<!-- JavaScript -->
<script src="/static/simplified_protocol_mapping.js"></script>
<script src="/static/simplified_discovery.js"></script>
</body>
</html>

202
test_api_integration.py Normal file
View File

@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""
Test API Integration for Simplified Protocol Signals
"""
import sys
import os
import asyncio
import json
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.dashboard.simplified_models import ProtocolSignalCreate, ProtocolType
from src.dashboard.simplified_configuration_manager import simplified_configuration_manager
async def test_api_endpoints():
"""Test the API endpoints through the configuration manager"""
print("\n=== Testing API Integration ===")
# Test 1: Create signals
print("\n1. Creating test signals:")
test_signals = [
{
"signal_name": "Boiler Temperature Reading",
"tags": ["equipment:boiler", "protocol:modbus_tcp", "data_point:temperature", "unit:celsius"],
"protocol_type": "modbus_tcp",
"protocol_address": "30001",
"db_source": "measurements.boiler_temperature"
},
{
"signal_name": "Pump Motor Status",
"tags": ["equipment:pump", "protocol:opcua", "data_point:status", "type:boolean"],
"protocol_type": "opcua",
"protocol_address": "ns=2;s=PumpStatus",
"db_source": "measurements.pump_status"
},
{
"signal_name": "System Pressure",
"tags": ["equipment:system", "protocol:modbus_tcp", "data_point:pressure", "unit:psi"],
"protocol_type": "modbus_tcp",
"protocol_address": "30002",
"db_source": "measurements.system_pressure"
}
]
created_signals = []
for signal_data in test_signals:
signal_create = ProtocolSignalCreate(
signal_name=signal_data["signal_name"],
tags=signal_data["tags"],
protocol_type=ProtocolType(signal_data["protocol_type"]),
protocol_address=signal_data["protocol_address"],
db_source=signal_data["db_source"]
)
success = simplified_configuration_manager.add_protocol_signal(signal_create)
if success:
# Get the actual signal ID that was used
signal_id = signal_create.generate_signal_id()
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
if signal:
created_signals.append(signal)
print(f" ✓ Created: {signal.signal_name}")
else:
print(f" ⚠ Created but cannot retrieve: {signal_data['signal_name']}")
else:
print(f" ✗ Failed to create: {signal_data['signal_name']}")
# Test 2: Get all signals
print("\n2. Getting all signals:")
all_signals = simplified_configuration_manager.get_protocol_signals()
print(f" Total signals: {len(all_signals)}")
for signal in all_signals:
print(f" - {signal.signal_name} ({signal.protocol_type.value})")
# Test 3: Filter by tags
print("\n3. Filtering by tags:")
modbus_signals = simplified_configuration_manager.search_signals_by_tags(["protocol:modbus_tcp"])
print(f" Modbus signals: {len(modbus_signals)}")
for signal in modbus_signals:
print(f" - {signal.signal_name}")
# Test 4: Get all tags
print("\n4. Getting all tags:")
all_tags = simplified_configuration_manager.get_all_tags()
print(f" All tags: {all_tags}")
# Test 5: Update a signal
print("\n5. Updating a signal:")
if created_signals:
signal_to_update = created_signals[0]
print(f" Updating: {signal_to_update.signal_name}")
from src.dashboard.simplified_models import ProtocolSignalUpdate
update_data = ProtocolSignalUpdate(
signal_name="Updated Boiler Temperature",
tags=["equipment:boiler", "protocol:modbus_tcp", "data_point:temperature", "unit:celsius", "updated:true"]
)
success = simplified_configuration_manager.update_protocol_signal(signal_to_update.signal_id, update_data)
if success:
updated_signal = simplified_configuration_manager.get_protocol_signal(signal_to_update.signal_id)
print(f" ✓ Updated to: {updated_signal.signal_name}")
print(f" New tags: {updated_signal.tags}")
else:
print(f" ✗ Failed to update")
# Test 6: Delete a signal
print("\n6. Deleting a signal:")
if len(created_signals) > 1:
signal_to_delete = created_signals[1]
print(f" Deleting: {signal_to_delete.signal_name}")
success = simplified_configuration_manager.delete_protocol_signal(signal_to_delete.signal_id)
if success:
print(f" ✓ Deleted successfully")
else:
print(f" ✗ Failed to delete")
# Test 7: Get remaining signals
print("\n7. Final signal count:")
final_signals = simplified_configuration_manager.get_protocol_signals()
print(f" Remaining signals: {len(final_signals)}")
return len(final_signals) > 0
def test_api_compatibility():
"""Test that the new API is compatible with discovery results"""
print("\n=== Testing Discovery Compatibility ===")
from src.dashboard.simplified_models import SignalDiscoveryResult
# Simulate discovery results
discovery_results = [
{
"device_name": "Flow Meter",
"protocol_type": "modbus_tcp",
"protocol_address": "30003",
"data_point": "Flow Rate",
"device_address": "192.168.1.105"
},
{
"device_name": "Level Sensor",
"protocol_type": "opcua",
"protocol_address": "ns=2;s=Level",
"data_point": "Tank Level",
"device_address": "192.168.1.106"
}
]
for discovery_data in discovery_results:
discovery = SignalDiscoveryResult(**discovery_data)
signal_create = discovery.to_protocol_signal_create()
print(f"\nDiscovery: {discovery.device_name}")
print(f" Signal Name: {signal_create.signal_name}")
print(f" Tags: {signal_create.tags}")
print(f" Protocol: {signal_create.protocol_type.value}")
print(f" Address: {signal_create.protocol_address}")
print(f" DB Source: {signal_create.db_source}")
# Validate
validation = simplified_configuration_manager.validate_signal_configuration(signal_create)
print(f" Valid: {validation['valid']}")
if validation['warnings']:
print(f" Warnings: {validation['warnings']}")
def main():
"""Run all API integration tests"""
print("Calejo Control API Integration Test")
print("=" * 50)
try:
# Run async tests
success = asyncio.run(test_api_endpoints())
# Run compatibility tests
test_api_compatibility()
print("\n" + "=" * 50)
if success:
print("✅ All API integration tests completed successfully!")
print("\nAPI Endpoints Available:")
print(" • GET /api/v1/dashboard/protocol-signals")
print(" • GET /api/v1/dashboard/protocol-signals/{signal_id}")
print(" • POST /api/v1/dashboard/protocol-signals")
print(" • PUT /api/v1/dashboard/protocol-signals/{signal_id}")
print(" • DELETE /api/v1/dashboard/protocol-signals/{signal_id}")
print(" • GET /api/v1/dashboard/protocol-signals/tags/all")
else:
print("❌ Some API integration tests failed")
return 1
except Exception as e:
print(f"\n❌ API integration test failed: {e}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

329
test_discovery.js Normal file
View File

@ -0,0 +1,329 @@
// Test script to verify discovery functionality
// This simulates the browser environment and tests the discovery system
// Mock browser environment
let modalDisplay = 'none';
const mockDocument = {
getElementById: function(id) {
console.log(`getElementById called with: ${id}`);
// Mock the modal
if (id === 'mapping-modal') {
return {
style: {
display: modalDisplay,
set display(value) {
modalDisplay = value;
console.log(`Modal display set to: ${value}`);
},
get display() {
return modalDisplay;
}
},
innerHTML: 'Mock modal content'
};
}
// Mock form fields
const mockFields = {
'mapping_id': { value: '' },
'protocol_type': { value: '', dispatchEvent: () => console.log('protocol_type change event') },
'protocol_address': { value: '' },
'station_id': {
value: '',
options: [{ value: '', textContent: 'Select Station' }, { value: 'station_main', textContent: 'Main Pump Station' }],
dispatchEvent: () => console.log('station_id change event')
},
'equipment_id': {
value: '',
options: [{ value: '', textContent: 'Select Equipment' }, { value: 'pump_primary', textContent: 'Primary Pump' }]
},
'data_type_id': {
value: '',
options: [{ value: '', textContent: 'Select Data Type' }, { value: 'speed_pump', textContent: 'Pump Speed' }]
},
'db_source': { value: '' }
};
return mockFields[id] || null;
},
querySelector: function(selector) {
console.log(`querySelector called with: ${selector}`);
return null;
},
querySelectorAll: function(selector) {
console.log(`querySelectorAll called with: ${selector}`);
return [];
}
};
// Mock global document
const document = mockDocument;
// Mock showAddMappingModal function
const showAddMappingModal = function() {
console.log('showAddMappingModal called');
const modal = document.getElementById('mapping-modal');
if (modal) {
modal.style.display = 'block';
console.log('Modal opened successfully');
}
};
// Import the discovery class (simplified version for testing)
class ProtocolDiscovery {
constructor() {
this.currentScanId = 'test-scan-123';
this.isScanning = false;
}
// Test the populateProtocolForm method
populateProtocolForm(endpoint) {
console.log('\n=== Testing populateProtocolForm ===');
// Create a new protocol mapping ID
const mappingId = `${endpoint.device_id}_${endpoint.protocol_type}`;
// Get default metadata IDs
const defaultStationId = this.getDefaultStationId();
const defaultEquipmentId = this.getDefaultEquipmentId(defaultStationId);
const defaultDataTypeId = this.getDefaultDataTypeId();
// Set form values
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: defaultStationId,
equipment_id: defaultEquipmentId,
data_type_id: defaultDataTypeId
};
console.log('Form data created:', formData);
// Auto-populate the protocol mapping form
this.autoPopulateProtocolForm(formData);
}
autoPopulateProtocolForm(formData) {
console.log('\n=== Testing autoPopulateProtocolForm ===');
console.log('Auto-populating protocol form with:', formData);
// First, open the "Add New Mapping" modal
this.openAddMappingModal();
// Wait for modal to be fully loaded and visible
const waitForModal = setInterval(() => {
const modal = document.getElementById('mapping-modal');
const isModalVisible = modal && modal.style.display !== 'none';
if (isModalVisible) {
clearInterval(waitForModal);
this.populateModalFields(formData);
}
}, 50);
// Timeout after 2 seconds
setTimeout(() => {
clearInterval(waitForModal);
const modal = document.getElementById('mapping-modal');
if (modal && modal.style.display !== 'none') {
this.populateModalFields(formData);
} else {
console.error('Modal did not open within timeout period');
}
}, 2000);
}
populateModalFields(formData) {
console.log('\n=== Testing populateModalFields ===');
console.log('Populating modal fields with:', formData);
// 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 stationIdField = document.getElementById('station_id');
const equipmentIdField = document.getElementById('equipment_id');
const dataTypeIdField = document.getElementById('data_type_id');
const dbSourceField = document.getElementById('db_source');
console.log('Found fields:', {
mappingIdField: !!mappingIdField,
protocolTypeField: !!protocolTypeField,
protocolAddressField: !!protocolAddressField,
stationIdField: !!stationIdField,
equipmentIdField: !!equipmentIdField,
dataTypeIdField: !!dataTypeIdField,
dbSourceField: !!dbSourceField
});
// Populate mapping ID
if (mappingIdField) {
mappingIdField.value = formData.mapping_id;
console.log('✓ Set mapping_id to:', formData.mapping_id);
}
// Populate protocol type
if (protocolTypeField) {
protocolTypeField.value = formData.protocol_type;
console.log('✓ Set protocol_type to:', formData.protocol_type);
// Trigger protocol field updates
protocolTypeField.dispatchEvent(new Event('change'));
}
// Populate protocol address
if (protocolAddressField) {
protocolAddressField.value = formData.protocol_address;
console.log('✓ Set protocol_address to:', formData.protocol_address);
}
// Set station, equipment, and data type
if (stationIdField) {
this.waitForStationsLoaded(() => {
if (this.isValidStationId(formData.station_id)) {
stationIdField.value = formData.station_id;
console.log('✓ Set station_id to:', formData.station_id);
// Trigger equipment dropdown update
stationIdField.dispatchEvent(new Event('change'));
// Wait for equipment to be loaded
setTimeout(() => {
if (equipmentIdField && this.isValidEquipmentId(formData.equipment_id)) {
equipmentIdField.value = formData.equipment_id;
console.log('✓ Set equipment_id to:', formData.equipment_id);
}
if (dataTypeIdField && this.isValidDataTypeId(formData.data_type_id)) {
dataTypeIdField.value = formData.data_type_id;
console.log('✓ Set data_type_id to:', formData.data_type_id);
}
// Set default database source
if (dbSourceField && !dbSourceField.value) {
dbSourceField.value = 'measurements.' + formData.device_name.toLowerCase().replace(/[^a-z0-9]/g, '_');
console.log('✓ Set db_source to:', dbSourceField.value);
}
console.log('\n✅ Protocol form successfully populated!');
console.log('All fields should now be filled with discovery data.');
}, 100);
}
});
}
}
openAddMappingModal() {
console.log('\n=== Testing openAddMappingModal ===');
console.log('Attempting to open Add New Mapping modal...');
// First try to use the global function
if (typeof showAddMappingModal === 'function') {
console.log('✓ Using showAddMappingModal function');
showAddMappingModal();
return;
}
console.log('❌ Could not find any way to open the protocol mapping modal');
}
getDefaultProtocolAddress(endpoint) {
const protocolType = endpoint.protocol_type;
switch (protocolType) {
case 'modbus_tcp':
return '40001';
case 'opc_ua':
return 'ns=2;s=MyVariable';
case 'modbus_rtu':
return '40001';
case 'rest_api':
return '/api/v1/data/endpoint';
default:
return 'unknown';
}
}
getDefaultStationId() {
const stationSelect = document.getElementById('station_id');
if (stationSelect && stationSelect.options.length > 1) {
return stationSelect.options[1].value;
}
return 'station_main';
}
getDefaultEquipmentId(stationId) {
const equipmentSelect = document.getElementById('equipment_id');
if (equipmentSelect && equipmentSelect.options.length > 1) {
return equipmentSelect.options[1].value;
}
if (stationId === 'station_main') return 'pump_primary';
if (stationId === 'station_backup') return 'pump_backup';
if (stationId === 'station_control') return 'controller_plc';
return 'pump_primary';
}
getDefaultDataTypeId() {
const dataTypeSelect = document.getElementById('data_type_id');
if (dataTypeSelect && dataTypeSelect.options.length > 1) {
return dataTypeSelect.options[1].value;
}
return 'speed_pump';
}
isValidStationId(stationId) {
const stationSelect = document.getElementById('station_id');
if (!stationSelect) return false;
return Array.from(stationSelect.options).some(option => option.value === stationId);
}
isValidEquipmentId(equipmentId) {
const equipmentSelect = document.getElementById('equipment_id');
if (!equipmentSelect) return false;
return Array.from(equipmentSelect.options).some(option => option.value === equipmentId);
}
isValidDataTypeId(dataTypeId) {
const dataTypeSelect = document.getElementById('data_type_id');
if (!dataTypeSelect) return false;
return Array.from(dataTypeSelect.options).some(option => option.value === dataTypeId);
}
waitForStationsLoaded(callback, maxWait = 3000) {
const stationSelect = document.getElementById('station_id');
if (!stationSelect) {
console.error('Station select element not found');
callback();
return;
}
// Check if stations are already loaded
if (stationSelect.options.length > 1) {
console.log('✓ Stations already loaded:', stationSelect.options.length);
callback();
return;
}
console.log('Waiting for stations to load...');
callback(); // In test, just call immediately
}
}
// Run the test
console.log('🚀 Starting Protocol Discovery Test\n');
const discovery = new ProtocolDiscovery();
// Test with a sample discovered endpoint
const sampleEndpoint = {
device_id: 'device_001',
protocol_type: 'modbus_tcp',
device_name: 'Water Pump Controller',
address: '192.168.1.100',
port: 502
};
console.log('Testing with sample endpoint:', sampleEndpoint);
discovery.populateProtocolForm(sampleEndpoint);

328
test_discovery_simple.html Normal file
View File

@ -0,0 +1,328 @@
<!DOCTYPE html>
<html>
<head>
<title>Protocol Discovery Test</title>
<style>
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
.modal-content { background-color: white; margin: 15% auto; padding: 20px; border: 1px solid #888; width: 50%; }
.close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #007acc; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }
.alert { padding: 10px; border-radius: 4px; margin: 10px 0; }
.alert.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.alert.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
</style>
</head>
<body>
<h1>Protocol Discovery Test</h1>
<div style="border: 1px solid #ccc; padding: 20px; margin: 20px 0;">
<h2>Test Discovery "Use" Button</h2>
<button onclick="testDiscovery()">Test Discovery Use Button</button>
<button onclick="showAddMappingModal()">Open Modal Manually</button>
</div>
<!-- Add/Edit Mapping Modal -->
<div id="mapping-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeMappingModal()">&times;</span>
<h3 id="modal-title">Add Protocol Mapping</h3>
<form id="mapping-form">
<div class="form-group">
<label for="mapping_id">Mapping ID:</label>
<input type="text" id="mapping_id" name="mapping_id" required>
</div>
<div class="form-group">
<label for="protocol_type">Protocol Type:</label>
<select id="protocol_type" name="protocol_type" required onchange="updateProtocolFields()">
<option value="">Select Protocol</option>
<option value="modbus_tcp">Modbus TCP</option>
<option value="opcua">OPC UA</option>
<option value="modbus_rtu">Modbus RTU</option>
<option value="rest_api">REST API</option>
</select>
</div>
<div class="form-group">
<label for="station_id">Station:</label>
<select id="station_id" name="station_id" required>
<option value="">Select Station</option>
<option value="station_main">Main Pump Station</option>
<option value="station_backup">Backup Pump Station</option>
<option value="station_control">Control Station</option>
</select>
</div>
<div class="form-group">
<label for="equipment_id">Equipment:</label>
<select id="equipment_id" name="equipment_id" required>
<option value="">Select Equipment</option>
<option value="pump_primary">Primary Pump</option>
<option value="pump_backup">Backup Pump</option>
<option value="sensor_pressure">Pressure Sensor</option>
<option value="sensor_flow">Flow Meter</option>
<option value="valve_control">Control Valve</option>
<option value="controller_plc">PLC Controller</option>
</select>
</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>
<option value="speed_pump">Pump Speed</option>
<option value="pressure_water">Water Pressure</option>
<option value="status_pump">Pump Status</option>
<option value="flow_rate">Flow Rate</option>
<option value="position_valve">Valve Position</option>
<option value="emergency_stop">Emergency Stop</option>
</select>
</div>
<div class="form-group">
<label for="protocol_address">Protocol Address:</label>
<input type="text" id="protocol_address" name="protocol_address" required>
<small id="protocol_address_help" style="color: #666;"></small>
</div>
<div class="form-group">
<label for="db_source">Database Source:</label>
<input type="text" id="db_source" name="db_source" required placeholder="table.column">
</div>
<div class="action-buttons">
<button type="button" onclick="validateMapping()">Validate</button>
<button type="submit" style="background: #28a745;">Save Mapping</button>
<button type="button" onclick="closeMappingModal()" style="background: #dc3545;">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Notifications -->
<div id="discovery-notifications"></div>
<script>
// Modal functions
function showAddMappingModal() {
console.log('showAddMappingModal called');
document.getElementById('modal-title').textContent = 'Add Protocol Mapping';
document.getElementById('mapping-form').reset();
document.getElementById('protocol_address_help').textContent = '';
document.getElementById('mapping-modal').style.display = 'block';
}
function closeMappingModal() {
document.getElementById('mapping-modal').style.display = 'none';
}
function updateProtocolFields() {
const protocolType = document.getElementById('protocol_type').value;
const helpText = document.getElementById('protocol_address_help');
switch (protocolType) {
case 'modbus_tcp':
helpText.textContent = 'Modbus address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
break;
case 'opcua':
helpText.textContent = 'OPC UA NodeId format: ns=2;s=MyVariable or ns=2;i=1234';
break;
case 'modbus_rtu':
helpText.textContent = 'Modbus RTU address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
break;
case 'rest_api':
helpText.textContent = 'REST API endpoint format: /api/v1/data/endpoint';
break;
default:
helpText.textContent = '';
}
}
function validateMapping() {
alert('Mapping validation would be performed here');
}
// Test function
function testDiscovery() {
console.log('Testing discovery functionality...');
// Simulate a discovered endpoint
const endpoint = {
device_id: 'device_001',
protocol_type: 'modbus_tcp',
device_name: 'Water Pump Controller',
address: '192.168.1.100',
port: 502
};
// Create a new protocol mapping ID
const mappingId = `${endpoint.device_id}_${endpoint.protocol_type}`;
// Get default metadata IDs
const defaultStationId = 'station_main';
const defaultEquipmentId = 'pump_primary';
const defaultDataTypeId = 'speed_pump';
// Set form values
const formData = {
mapping_id: mappingId,
protocol_type: endpoint.protocol_type === 'opc_ua' ? 'opcua' : endpoint.protocol_type,
protocol_address: '40001',
device_name: endpoint.device_name || endpoint.device_id,
device_address: endpoint.address,
device_port: endpoint.port || '',
station_id: defaultStationId,
equipment_id: defaultEquipmentId,
data_type_id: defaultDataTypeId
};
console.log('Form data created:', formData);
// Auto-populate the protocol mapping form
autoPopulateProtocolForm(formData);
}
function autoPopulateProtocolForm(formData) {
console.log('Auto-populating protocol form with:', formData);
// First, open the "Add New Mapping" modal
showAddMappingModal();
// Wait for modal to be fully loaded and visible
const waitForModal = setInterval(() => {
const modal = document.getElementById('mapping-modal');
const isModalVisible = modal && modal.style.display !== 'none';
if (isModalVisible) {
clearInterval(waitForModal);
populateModalFields(formData);
}
}, 50);
// Timeout after 2 seconds
setTimeout(() => {
clearInterval(waitForModal);
const modal = document.getElementById('mapping-modal');
if (modal && modal.style.display !== 'none') {
populateModalFields(formData);
} else {
console.error('Modal did not open within timeout period');
showNotification('Could not open protocol mapping form. Please try opening it manually.', 'error');
}
}, 2000);
}
function populateModalFields(formData) {
console.log('Populating modal fields with:', formData);
// 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 stationIdField = document.getElementById('station_id');
const equipmentIdField = document.getElementById('equipment_id');
const dataTypeIdField = document.getElementById('data_type_id');
const dbSourceField = document.getElementById('db_source');
console.log('Found fields:', {
mappingIdField: !!mappingIdField,
protocolTypeField: !!protocolTypeField,
protocolAddressField: !!protocolAddressField,
stationIdField: !!stationIdField,
equipmentIdField: !!equipmentIdField,
dataTypeIdField: !!dataTypeIdField,
dbSourceField: !!dbSourceField
});
// Populate mapping ID
if (mappingIdField) {
mappingIdField.value = formData.mapping_id;
console.log('✓ Set mapping_id to:', formData.mapping_id);
}
// Populate protocol type
if (protocolTypeField) {
protocolTypeField.value = formData.protocol_type;
console.log('✓ Set protocol_type to:', formData.protocol_type);
// Trigger protocol field updates
protocolTypeField.dispatchEvent(new Event('change'));
}
// Populate protocol address
if (protocolAddressField) {
protocolAddressField.value = formData.protocol_address;
console.log('✓ Set protocol_address to:', formData.protocol_address);
}
// Set station, equipment, and data type
if (stationIdField) {
if (isValidStationId(formData.station_id)) {
stationIdField.value = formData.station_id;
console.log('✓ Set station_id to:', formData.station_id);
// Trigger equipment dropdown update
stationIdField.dispatchEvent(new Event('change'));
// Wait for equipment to be loaded
setTimeout(() => {
if (equipmentIdField && isValidEquipmentId(formData.equipment_id)) {
equipmentIdField.value = formData.equipment_id;
console.log('✓ Set equipment_id to:', formData.equipment_id);
}
if (dataTypeIdField && isValidDataTypeId(formData.data_type_id)) {
dataTypeIdField.value = formData.data_type_id;
console.log('✓ Set data_type_id to:', formData.data_type_id);
}
// Set default database source
if (dbSourceField && !dbSourceField.value) {
dbSourceField.value = 'measurements.' + formData.device_name.toLowerCase().replace(/[^a-z0-9]/g, '_');
console.log('✓ Set db_source to:', dbSourceField.value);
}
// Show success message
showNotification(`Protocol form populated with ${formData.device_name}. Please review and complete any missing information.`, 'success');
}, 100);
}
}
}
function isValidStationId(stationId) {
const stationSelect = document.getElementById('station_id');
if (!stationSelect) return false;
return Array.from(stationSelect.options).some(option => option.value === stationId);
}
function isValidEquipmentId(equipmentId) {
const equipmentSelect = document.getElementById('equipment_id');
if (!equipmentSelect) return false;
return Array.from(equipmentSelect.options).some(option => option.value === equipmentId);
}
function isValidDataTypeId(dataTypeId) {
const dataTypeSelect = document.getElementById('data_type_id');
if (!dataTypeSelect) return false;
return Array.from(dataTypeSelect.options).some(option => option.value === dataTypeId);
}
function showNotification(message, type = 'info') {
const alertClass = {
'success': 'alert-success',
'error': 'alert-danger',
'warning': 'alert-warning',
'info': 'alert-info'
}[type] || 'alert-info';
const notification = document.createElement('div');
notification.className = `alert ${alertClass}`;
notification.innerHTML = message;
const container = document.getElementById('discovery-notifications');
container.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
</script>
</body>
</html>

View File

@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
Test the integration between discovery and simplified protocol mapping system
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
from src.dashboard.simplified_models import ProtocolSignalCreate, ProtocolType
from src.dashboard.simplified_configuration_manager import simplified_configuration_manager
def test_discovery_to_signal_workflow():
"""Test the complete workflow from discovery to signal creation"""
print("=" * 60)
print("Testing Discovery to Protocol Signal Integration")
print("=" * 60)
# Simulate discovery results
discovery_results = [
{
"device_name": "Boiler Temperature Sensor",
"protocol_type": "opcua",
"protocol_address": "ns=2;s=Temperature",
"data_point": "Temperature",
"device_address": "192.168.1.100"
},
{
"device_name": "Main Water Pump",
"protocol_type": "modbus_tcp",
"protocol_address": "40001",
"data_point": "Speed",
"device_address": "192.168.1.101"
},
{
"device_name": "System Pressure Sensor",
"protocol_type": "modbus_tcp",
"protocol_address": "40002",
"data_point": "Pressure",
"device_address": "192.168.1.102"
}
]
print("\n1. Discovery Results:")
for i, device in enumerate(discovery_results, 1):
print(f" {i}. {device['device_name']} - {device['protocol_type']} - {device['protocol_address']}")
# Convert discovery results to signal format
print("\n2. Converting Discovery to Signal Format:")
signals_created = []
for device in discovery_results:
# Generate signal name
signal_name = f"{device['device_name']} {device['data_point']}"
# Generate tags
tags = [
f"device:{device['device_name'].lower().replace(' ', '_')}",
f"protocol:{device['protocol_type']}",
f"data_point:{device['data_point'].lower().replace(' ', '_')}",
f"address:{device['device_address']}",
"discovered:true"
]
# Generate database source
db_source = f"measurements.{device['device_name'].lower().replace(' ', '_')}_{device['data_point'].lower().replace(' ', '_')}"
# Create signal
signal_create = ProtocolSignalCreate(
signal_name=signal_name,
tags=tags,
protocol_type=ProtocolType(device['protocol_type']),
protocol_address=device['protocol_address'],
db_source=db_source
)
# Add to configuration manager
success = simplified_configuration_manager.add_protocol_signal(signal_create)
if success:
signals_created.append(signal_create)
print(f" ✓ Created: {signal_name}")
print(f" Tags: {', '.join(tags)}")
print(f" Protocol: {device['protocol_type']} at {device['protocol_address']}")
print(f" DB Source: {db_source}")
else:
print(f" ✗ Failed to create: {signal_name}")
# Test filtering and retrieval
print("\n3. Testing Signal Management:")
# Get all signals
all_signals = simplified_configuration_manager.get_protocol_signals()
print(f" Total signals: {len(all_signals)}")
# Filter by protocol
modbus_signals = [s for s in all_signals if 'protocol:modbus_tcp' in s.tags]
print(f" Modbus TCP signals: {len(modbus_signals)}")
# Filter by device
boiler_signals = [s for s in all_signals if 'device:boiler_temperature_sensor' in s.tags]
print(f" Boiler signals: {len(boiler_signals)}")
# Get all tags
all_tags = simplified_configuration_manager.get_all_tags()
print(f" All tags: {len(all_tags)} unique tags")
# Test signal updates
print("\n4. Testing Signal Updates:")
if signals_created:
first_signal = signals_created[0]
signal_id = first_signal.generate_signal_id()
# Get the signal
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
if signal:
print(f" Retrieved signal: {signal.signal_name}")
# Update the signal
updated_tags = signal.tags + ["unit:celsius", "alarm:high_temp"]
update_success = simplified_configuration_manager.update_protocol_signal(
signal_id,
tags=updated_tags,
preprocessing_enabled=True
)
if update_success:
print(f" ✓ Updated signal with new tags and preprocessing")
updated_signal = simplified_configuration_manager.get_protocol_signal(signal_id)
print(f" New tags: {', '.join(updated_signal.tags)}")
print(f" Preprocessing: {updated_signal.preprocessing_enabled}")
else:
print(f" ✗ Failed to update signal")
# Test signal deletion
print("\n5. Testing Signal Deletion:")
if signals_created:
last_signal = signals_created[-1]
signal_id = last_signal.generate_signal_id()
delete_success = simplified_configuration_manager.delete_protocol_signal(signal_id)
if delete_success:
print(f" ✓ Deleted signal: {last_signal.signal_name}")
remaining_signals = simplified_configuration_manager.get_protocol_signals()
print(f" Remaining signals: {len(remaining_signals)}")
else:
print(f" ✗ Failed to delete signal")
print("\n" + "=" * 60)
print("Integration Test Results:")
print(f" - Discovery devices processed: {len(discovery_results)}")
print(f" - Signals successfully created: {len(signals_created)}")
print(f" - Final signal count: {len(simplified_configuration_manager.get_protocol_signals())}")
print(f" - Unique tags available: {len(simplified_configuration_manager.get_all_tags())}")
if len(signals_created) == len(discovery_results):
print("\n✅ SUCCESS: All discovery devices successfully converted to protocol signals!")
print(" The simplified system is working correctly with discovery integration.")
else:
print("\n❌ FAILURE: Some discovery devices failed to convert to signals.")
print("=" * 60)
if __name__ == "__main__":
test_discovery_to_signal_workflow()

160
test_migration.py Normal file
View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""
Migration Test Script
Tests the simplified signal name + tags architecture
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.dashboard.simplified_models import (
ProtocolSignalCreate, ProtocolType, SignalDiscoveryResult
)
from src.dashboard.simplified_configuration_manager import simplified_configuration_manager
def test_simplified_models():
"""Test the new simplified models"""
print("\n=== Testing Simplified Models ===")
# Test 1: Create from discovery result
print("\n1. Testing discovery result conversion:")
discovery = SignalDiscoveryResult(
device_name="Water Pump Controller",
protocol_type=ProtocolType.MODBUS_TCP,
protocol_address="40001",
data_point="Speed",
device_address="192.168.1.100"
)
signal_create = discovery.to_protocol_signal_create()
print(f" Signal Name: {signal_create.signal_name}")
print(f" Tags: {signal_create.tags}")
print(f" Protocol: {signal_create.protocol_type}")
print(f" Address: {signal_create.protocol_address}")
print(f" DB Source: {signal_create.db_source}")
# Test 2: Validation
print("\n2. Testing validation:")
validation = simplified_configuration_manager.validate_signal_configuration(signal_create)
print(f" Valid: {validation['valid']}")
print(f" Errors: {validation['errors']}")
print(f" Warnings: {validation['warnings']}")
# Test 3: Add signal
print("\n3. Testing signal creation:")
success = simplified_configuration_manager.add_protocol_signal(signal_create)
print(f" Signal created: {success}")
# Test 4: Retrieve signals
print("\n4. Testing signal retrieval:")
signals = simplified_configuration_manager.get_protocol_signals()
print(f" Number of signals: {len(signals)}")
for signal in signals:
print(f" - {signal.signal_name} ({signal.signal_id})")
# Test 5: Tag-based filtering
print("\n5. Testing tag-based filtering:")
pump_signals = simplified_configuration_manager.search_signals_by_tags(["equipment:pump"])
print(f" Pump signals: {len(pump_signals)}")
# Test 6: All tags
print("\n6. Testing tag collection:")
all_tags = simplified_configuration_manager.get_all_tags()
print(f" All tags: {all_tags}")
def test_migration_scenarios():
"""Test various migration scenarios"""
print("\n=== Testing Migration Scenarios ===")
scenarios = [
{
"name": "Modbus Pump Speed",
"device_name": "Main Water Pump",
"protocol_type": ProtocolType.MODBUS_TCP,
"data_point": "Speed",
"protocol_address": "40001"
},
{
"name": "OPC UA Temperature",
"device_name": "Boiler Temperature Sensor",
"protocol_type": ProtocolType.OPCUA,
"data_point": "Temperature",
"protocol_address": "ns=2;s=Temperature"
},
{
"name": "REST API Status",
"device_name": "System Controller",
"protocol_type": ProtocolType.REST_API,
"data_point": "Status",
"protocol_address": "/api/v1/system/status"
}
]
for scenario in scenarios:
print(f"\nScenario: {scenario['name']}")
discovery = SignalDiscoveryResult(
device_name=scenario["device_name"],
protocol_type=scenario["protocol_type"],
protocol_address=scenario["protocol_address"],
data_point=scenario["data_point"]
)
signal_create = discovery.to_protocol_signal_create()
success = simplified_configuration_manager.add_protocol_signal(signal_create)
print(f" Created: {success}")
print(f" Signal: {signal_create.signal_name}")
print(f" Tags: {', '.join(signal_create.tags[:3])}...")
def compare_complexity():
"""Compare old vs new approach complexity"""
print("\n=== Complexity Comparison ===")
print("\nOLD APPROACH (Complex IDs):")
print(" Required fields:")
print(" - station_id: 'station_main'")
print(" - equipment_id: 'pump_primary'")
print(" - data_type_id: 'speed_pump'")
print(" - protocol_address: '40001'")
print(" - db_source: 'measurements.pump_speed'")
print(" Issues: Complex relationships, redundant IDs, confusing UX")
print("\nNEW APPROACH (Simple Names + Tags):")
print(" Required fields:")
print(" - signal_name: 'Main Water Pump Speed'")
print(" - tags: ['equipment:pump', 'protocol:modbus_tcp', 'data_point:speed']")
print(" - protocol_address: '40001'")
print(" - db_source: 'measurements.main_water_pump_speed'")
print(" Benefits: Intuitive, flexible, simpler relationships")
def main():
"""Run all tests"""
print("Calejo Control Migration Test")
print("=" * 50)
try:
test_simplified_models()
test_migration_scenarios()
compare_complexity()
print("\n" + "=" * 50)
print("✅ All migration tests completed successfully!")
print("\nMigration Benefits:")
print(" • Simplified user experience")
print(" • Flexible tag-based organization")
print(" • Intuitive signal names")
print(" • Reduced complexity")
print(" • Better discovery integration")
except Exception as e:
print(f"\n❌ Migration test failed: {e}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

273
test_simplified_ui.html Normal file
View File

@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Simplified UI</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f7fa;
}
.test-container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.test-button {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
.test-button:hover {
background: #5a6fd8;
}
.test-result {
margin: 10px 0;
padding: 10px;
border-radius: 5px;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
</head>
<body>
<div class="test-container">
<h1>Simplified Protocol Signals UI Test</h1>
<p>This test verifies the simplified UI components work correctly.</p>
<div id="test-results"></div>
<h3>Test Actions:</h3>
<button class="test-button" onclick="testModal()">Test Modal Opening</button>
<button class="test-button" onclick="testFormPopulation()">Test Form Population</button>
<button class="test-button" onclick="testDiscoveryIntegration()">Test Discovery Integration</button>
<button class="test-button" onclick="testAll()">Run All Tests</button>
</div>
<!-- Simplified Modal for Testing -->
<div id="signal-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Test Protocol Signal</h2>
<span class="close" onclick="closeSignalModal()">&times;</span>
</div>
<form id="signal-form">
<div class="form-group">
<label for="signal_name">Signal Name *</label>
<input type="text" id="signal_name" name="signal_name" required>
</div>
<div class="form-group">
<label for="tags">Tags</label>
<input type="text" id="tags" name="tags" placeholder="equipment:pump, protocol:modbus_tcp">
</div>
<div class="form-group">
<label for="protocol_type">Protocol Type *</label>
<select id="protocol_type" name="protocol_type" required>
<option value="">Select Protocol Type</option>
<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" required>
</div>
<div class="form-group">
<label for="db_source">Database Source *</label>
<input type="text" id="db_source" name="db_source" required>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeSignalModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Signal</button>
</div>
</form>
</div>
</div>
<script>
function logTest(message, type = 'success') {
const results = document.getElementById('test-results');
const resultDiv = document.createElement('div');
resultDiv.className = `test-result ${type}`;
resultDiv.textContent = message;
results.appendChild(resultDiv);
}
function showAddSignalModal() {
document.getElementById('signal-modal').style.display = 'block';
logTest('✓ Modal opened successfully');
}
function closeSignalModal() {
document.getElementById('signal-modal').style.display = 'none';
logTest('✓ Modal closed successfully');
}
function autoPopulateSignalForm(discoveryData) {
console.log('Auto-populating signal form with:', discoveryData);
// First, open the modal
showAddSignalModal();
// Wait for modal to be fully loaded and visible
const waitForModal = setInterval(() => {
const modal = document.getElementById('signal-modal');
const isModalVisible = modal && modal.style.display !== 'none';
if (isModalVisible) {
clearInterval(waitForModal);
populateModalFields(discoveryData);
}
}, 50);
}
function populateModalFields(discoveryData) {
console.log('Populating modal fields with:', discoveryData);
// Populate signal name
const signalNameField = document.getElementById('signal_name');
if (signalNameField && discoveryData.signal_name) {
signalNameField.value = discoveryData.signal_name;
console.log('✓ Set signal_name to:', discoveryData.signal_name);
}
// Populate tags
const tagsField = document.getElementById('tags');
if (tagsField && discoveryData.tags) {
tagsField.value = discoveryData.tags.join(', ');
console.log('✓ Set tags to:', discoveryData.tags);
}
// Populate protocol type
const protocolTypeField = document.getElementById('protocol_type');
if (protocolTypeField && discoveryData.protocol_type) {
protocolTypeField.value = discoveryData.protocol_type;
console.log('✓ Set protocol_type to:', discoveryData.protocol_type);
}
// Populate protocol address
const protocolAddressField = document.getElementById('protocol_address');
if (protocolAddressField && discoveryData.protocol_address) {
protocolAddressField.value = discoveryData.protocol_address;
console.log('✓ Set protocol_address to:', discoveryData.protocol_address);
}
// Populate database source
const dbSourceField = document.getElementById('db_source');
if (dbSourceField && discoveryData.db_source) {
dbSourceField.value = discoveryData.db_source;
console.log('✓ Set db_source to:', discoveryData.db_source);
}
logTest('✓ Form populated with discovery data successfully');
}
function testModal() {
logTest('Testing modal functionality...');
showAddSignalModal();
setTimeout(() => {
closeSignalModal();
}, 2000);
}
function testFormPopulation() {
logTest('Testing form population...');
const testData = {
signal_name: "Water Pump Controller Speed",
tags: ["equipment:pump", "protocol:modbus_tcp", "data_point:speed"],
protocol_type: "modbus_tcp",
protocol_address: "40001",
db_source: "measurements.water_pump_speed"
};
autoPopulateSignalForm(testData);
}
function testDiscoveryIntegration() {
logTest('Testing discovery integration...');
// Simulate discovery result
const discoveryResult = {
device_name: "Boiler Temperature Sensor",
protocol_type: "opcua",
protocol_address: "ns=2;s=Temperature",
data_point: "Temperature",
device_address: "192.168.1.100"
};
// Convert to signal format
const signalData = {
signal_name: `${discoveryResult.device_name} ${discoveryResult.data_point}`,
tags: [
`device:${discoveryResult.device_name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`,
`protocol:${discoveryResult.protocol_type}`,
`data_point:${discoveryResult.data_point.toLowerCase().replace(/[^a-z0-9]/g, '_')}`,
'discovered:true'
],
protocol_type: discoveryResult.protocol_type,
protocol_address: discoveryResult.protocol_address,
db_source: `measurements.${discoveryResult.device_name.toLowerCase().replace(/[^a-z0-9]/g, '_')}_${discoveryResult.data_point.toLowerCase().replace(/[^a-z0-9]/g, '_')}`
};
autoPopulateSignalForm(signalData);
logTest('✓ Discovery integration test completed');
}
function testAll() {
logTest('Running all tests...');
setTimeout(() => {
testModal();
}, 500);
setTimeout(() => {
testFormPopulation();
}, 3000);
setTimeout(() => {
testDiscoveryIntegration();
}, 6000);
setTimeout(() => {
logTest('All tests completed successfully!', 'success');
}, 9000);
}
// Initialize form submission handler
document.addEventListener('DOMContentLoaded', function() {
const signalForm = document.getElementById('signal-form');
if (signalForm) {
signalForm.addEventListener('submit', function(event) {
event.preventDefault();
logTest('✓ Form submitted successfully');
closeSignalModal();
});
}
});
</script>
</body>
</html>