feat: Implement configurable pump control preprocessing logic #5
|
|
@ -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
|
||||
|
|
@ -939,6 +939,229 @@ async def delete_protocol_mapping(mapping_id: str):
|
|||
|
||||
# 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
|
||||
|
||||
@dashboard_router.get("/metadata/summary")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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()">×</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
|
|
@ -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 editingMappingId = null;
|
||||
let tagMetadata = {
|
||||
stations: [],
|
||||
equipment: [],
|
||||
dataTypes: []
|
||||
};
|
||||
let editingSignalId = null;
|
||||
let allTags = new Set();
|
||||
|
||||
// Tag Metadata Functions
|
||||
async function loadTagMetadata() {
|
||||
// Simplified Signal Management Functions
|
||||
async function loadAllSignals() {
|
||||
try {
|
||||
// Load stations
|
||||
const stationsResponse = await fetch('/api/v1/dashboard/metadata/stations');
|
||||
const stationsData = await stationsResponse.json();
|
||||
if (stationsData.success) {
|
||||
tagMetadata.stations = stationsData.stations;
|
||||
populateStationDropdown();
|
||||
}
|
||||
|
||||
// Load data types
|
||||
const dataTypesResponse = await fetch('/api/v1/dashboard/metadata/data-types');
|
||||
const dataTypesData = await dataTypesResponse.json();
|
||||
if (dataTypesData.success) {
|
||||
tagMetadata.dataTypes = dataTypesData.data_types;
|
||||
populateDataTypeDropdown();
|
||||
}
|
||||
|
||||
// Load equipment for all stations
|
||||
const equipmentResponse = await fetch('/api/v1/dashboard/metadata/equipment');
|
||||
const equipmentData = await equipmentResponse.json();
|
||||
if (equipmentData.success) {
|
||||
tagMetadata.equipment = equipmentData.equipment;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading tag metadata:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function populateStationDropdown() {
|
||||
const stationSelect = document.getElementById('station_id');
|
||||
stationSelect.innerHTML = '<option value="">Select Station</option>';
|
||||
|
||||
tagMetadata.stations.forEach(station => {
|
||||
const option = document.createElement('option');
|
||||
option.value = station.id;
|
||||
option.textContent = `${station.name} (${station.id})`;
|
||||
stationSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function populateEquipmentDropdown(stationId = null) {
|
||||
const equipmentSelect = document.getElementById('equipment_id');
|
||||
equipmentSelect.innerHTML = '<option value="">Select Equipment</option>';
|
||||
|
||||
let filteredEquipment = tagMetadata.equipment;
|
||||
if (stationId) {
|
||||
filteredEquipment = tagMetadata.equipment.filter(eq => eq.station_id === stationId);
|
||||
}
|
||||
|
||||
filteredEquipment.forEach(equipment => {
|
||||
const option = document.createElement('option');
|
||||
option.value = equipment.id;
|
||||
option.textContent = `${equipment.name} (${equipment.id})`;
|
||||
equipmentSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function populateDataTypeDropdown() {
|
||||
const dataTypeSelect = document.getElementById('data_type_id');
|
||||
dataTypeSelect.innerHTML = '<option value="">Select Data Type</option>';
|
||||
|
||||
tagMetadata.dataTypes.forEach(dataType => {
|
||||
const option = document.createElement('option');
|
||||
option.value = dataType.id;
|
||||
option.textContent = `${dataType.name} (${dataType.id})`;
|
||||
if (dataType.units) {
|
||||
option.textContent += ` [${dataType.units}]`;
|
||||
}
|
||||
dataTypeSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Event listener for station selection change
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const stationSelect = document.getElementById('station_id');
|
||||
if (stationSelect) {
|
||||
stationSelect.addEventListener('change', function() {
|
||||
const stationId = this.value;
|
||||
populateEquipmentDropdown(stationId);
|
||||
});
|
||||
}
|
||||
|
||||
// Load tag metadata when page loads
|
||||
loadTagMetadata();
|
||||
});
|
||||
|
||||
function selectProtocol(protocol) {
|
||||
currentProtocolFilter = protocol;
|
||||
|
||||
// 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 response = await fetch('/api/v1/dashboard/protocol-signals');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
displayProtocolMappings(data.mappings);
|
||||
displaySignals(data.signals);
|
||||
updateTagCloud(data.signals);
|
||||
} else {
|
||||
showProtocolMappingAlert('Failed to load protocol mappings', 'error');
|
||||
showSimplifiedAlert('Failed to load signals', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading protocol mappings:', error);
|
||||
showProtocolMappingAlert('Error loading protocol mappings', 'error');
|
||||
console.error('Error loading signals:', error);
|
||||
showSimplifiedAlert('Error loading signals', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function displayProtocolMappings(mappings) {
|
||||
const tbody = document.getElementById('protocol-mappings-body');
|
||||
function displaySignals(signals) {
|
||||
const tbody = document.getElementById('protocol-signals-body');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (mappings.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px;">No protocol mappings found</td></tr>';
|
||||
if (signals.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 20px;">No protocol signals found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
mappings.forEach(mapping => {
|
||||
// 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 || '-');
|
||||
|
||||
signals.forEach(signal => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.id}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.protocol_type}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${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;">${signal.signal_name}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${signal.protocol_type}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">
|
||||
<button onclick="editMapping('${mapping.id}')" style="background: #007acc; margin-right: 5px;">Edit</button>
|
||||
<button onclick="deleteMapping('${mapping.id}')" style="background: #dc3545;">Delete</button>
|
||||
${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 showAddMappingModal() {
|
||||
editingMappingId = null;
|
||||
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 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 showEditMappingModal(mapping) {
|
||||
editingMappingId = mapping.id;
|
||||
document.getElementById('modal-title').textContent = 'Edit Protocol Mapping';
|
||||
document.getElementById('mapping_id').value = mapping.id;
|
||||
document.getElementById('protocol_type').value = mapping.protocol_type;
|
||||
|
||||
// Set dropdown values
|
||||
const stationSelect = document.getElementById('station_id');
|
||||
const equipmentSelect = document.getElementById('equipment_id');
|
||||
const dataTypeSelect = document.getElementById('data_type_id');
|
||||
|
||||
stationSelect.value = mapping.station_id || '';
|
||||
if (mapping.station_id) {
|
||||
populateEquipmentDropdown(mapping.station_id);
|
||||
function filterByTag(tag) {
|
||||
const filterInput = document.getElementById('tag-filter');
|
||||
if (filterInput) {
|
||||
filterInput.value = tag;
|
||||
applyFilters();
|
||||
}
|
||||
equipmentSelect.value = mapping.equipment_id || '';
|
||||
dataTypeSelect.value = mapping.data_type_id || '';
|
||||
}
|
||||
|
||||
document.getElementById('protocol_address').value = mapping.protocol_address;
|
||||
document.getElementById('db_source').value = mapping.db_source;
|
||||
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('mapping-modal').style.display = 'block';
|
||||
document.getElementById('signal-modal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeMappingModal() {
|
||||
document.getElementById('mapping-modal').style.display = 'none';
|
||||
editingMappingId = null;
|
||||
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');
|
||||
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 '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;
|
||||
|
|
@ -232,48 +160,22 @@ function updateProtocolFields() {
|
|||
}
|
||||
}
|
||||
|
||||
async function validateMapping() {
|
||||
const formData = getMappingFormData();
|
||||
|
||||
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) {
|
||||
// Form Submission
|
||||
async function saveSignal(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = getMappingFormData();
|
||||
const formData = getSignalFormData();
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (editingMappingId) {
|
||||
response = await fetch(`/api/v1/dashboard/protocol-mappings/${editingMappingId}`, {
|
||||
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-mappings', {
|
||||
response = await fetch('/api/v1/dashboard/protocol-signals', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
|
|
@ -283,76 +185,151 @@ async function saveMapping(event) {
|
|||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showProtocolMappingAlert(`Protocol mapping ${editingMappingId ? 'updated' : 'created'} successfully!`, 'success');
|
||||
closeMappingModal();
|
||||
loadProtocolMappings();
|
||||
showSimplifiedAlert(`Protocol signal ${editingSignalId ? 'updated' : 'created'} successfully!`, 'success');
|
||||
closeSignalModal();
|
||||
loadAllSignals();
|
||||
} else {
|
||||
showProtocolMappingAlert(`Failed to save mapping: ${data.detail || 'Unknown error'}`, 'error');
|
||||
showSimplifiedAlert(`Failed to save signal: ${data.detail || 'Unknown error'}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving mapping:', error);
|
||||
showProtocolMappingAlert('Error saving mapping', 'error');
|
||||
console.error('Error saving signal:', 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 {
|
||||
signal_name: document.getElementById('signal_name').value,
|
||||
tags: tags,
|
||||
protocol_type: document.getElementById('protocol_type').value,
|
||||
station_id: document.getElementById('station_id').value,
|
||||
equipment_id: document.getElementById('equipment_id').value,
|
||||
data_type_id: document.getElementById('data_type_id').value,
|
||||
protocol_address: document.getElementById('protocol_address').value,
|
||||
db_source: document.getElementById('db_source').value
|
||||
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 {
|
||||
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();
|
||||
|
||||
if (data.success) {
|
||||
const mapping = data.mappings.find(m => m.id === mappingId);
|
||||
if (mapping) {
|
||||
showEditMappingModal(mapping);
|
||||
showEditSignalModal(data.signal);
|
||||
} else {
|
||||
showProtocolMappingAlert('Mapping not found', 'error');
|
||||
}
|
||||
} else {
|
||||
showProtocolMappingAlert('Failed to load mapping', 'error');
|
||||
showSimplifiedAlert('Signal not found', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading mapping:', error);
|
||||
showProtocolMappingAlert('Error loading mapping', 'error');
|
||||
console.error('Error loading signal:', error);
|
||||
showSimplifiedAlert('Error loading signal', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMapping(mappingId) {
|
||||
if (!confirm(`Are you sure you want to delete mapping ${mappingId}?`)) {
|
||||
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-mappings/${mappingId}`, {
|
||||
const response = await fetch(`/api/v1/dashboard/protocol-signals/${signalId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showProtocolMappingAlert('Mapping deleted successfully!', 'success');
|
||||
loadProtocolMappings();
|
||||
showSimplifiedAlert('Signal deleted successfully!', 'success');
|
||||
loadAllSignals();
|
||||
} else {
|
||||
showProtocolMappingAlert(`Failed to delete mapping: ${data.detail || 'Unknown error'}`, 'error');
|
||||
showSimplifiedAlert(`Failed to delete signal: ${data.detail || 'Unknown error'}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting mapping:', error);
|
||||
showProtocolMappingAlert('Error deleting mapping', 'error');
|
||||
console.error('Error deleting signal:', error);
|
||||
showSimplifiedAlert('Error deleting signal', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showProtocolMappingAlert(message, type) {
|
||||
const alertsDiv = document.getElementById('protocol-mapping-alerts');
|
||||
// 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;
|
||||
|
|
@ -360,57 +337,21 @@ function showProtocolMappingAlert(message, type) {
|
|||
alertsDiv.innerHTML = '';
|
||||
alertsDiv.appendChild(alertDiv);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async function exportProtocolMappings() {
|
||||
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
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mappingForm = document.getElementById('mapping-form');
|
||||
if (mappingForm) {
|
||||
mappingForm.addEventListener('submit', saveMapping);
|
||||
const signalForm = document.getElementById('signal-form');
|
||||
if (signalForm) {
|
||||
signalForm.addEventListener('submit', saveSignal);
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
loadAllSignals();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()">×</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>
|
||||
|
|
@ -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())
|
||||
|
|
@ -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);
|
||||
|
|
@ -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()">×</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>
|
||||
|
|
@ -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()
|
||||
|
|
@ -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())
|
||||
|
|
@ -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()">×</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>
|
||||
Loading…
Reference in New Issue