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
|
# Protocol Discovery API Endpoints
|
||||||
|
|
||||||
|
# Simplified Protocol Signals API Endpoints
|
||||||
|
@dashboard_router.get("/protocol-signals")
|
||||||
|
async def get_protocol_signals(
|
||||||
|
tags: Optional[str] = None,
|
||||||
|
protocol_type: Optional[str] = None,
|
||||||
|
signal_name_contains: Optional[str] = None,
|
||||||
|
enabled: Optional[bool] = True
|
||||||
|
):
|
||||||
|
"""Get protocol signals with simplified name + tags approach"""
|
||||||
|
try:
|
||||||
|
from .simplified_models import ProtocolSignalFilter, ProtocolType
|
||||||
|
from .simplified_configuration_manager import simplified_configuration_manager
|
||||||
|
|
||||||
|
# Parse tags from comma-separated string
|
||||||
|
tag_list = tags.split(",") if tags else None
|
||||||
|
|
||||||
|
# Convert protocol_type string to enum if provided
|
||||||
|
protocol_enum = None
|
||||||
|
if protocol_type:
|
||||||
|
try:
|
||||||
|
protocol_enum = ProtocolType(protocol_type)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid protocol type: {protocol_type}")
|
||||||
|
|
||||||
|
# Create filter
|
||||||
|
filters = ProtocolSignalFilter(
|
||||||
|
tags=tag_list,
|
||||||
|
protocol_type=protocol_enum,
|
||||||
|
signal_name_contains=signal_name_contains,
|
||||||
|
enabled=enabled
|
||||||
|
)
|
||||||
|
|
||||||
|
signals = simplified_configuration_manager.get_protocol_signals(filters)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"signals": [signal.dict() for signal in signals],
|
||||||
|
"count": len(signals)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting protocol signals: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get protocol signals: {str(e)}")
|
||||||
|
|
||||||
|
@dashboard_router.get("/protocol-signals/{signal_id}")
|
||||||
|
async def get_protocol_signal(signal_id: str):
|
||||||
|
"""Get a specific protocol signal by ID"""
|
||||||
|
try:
|
||||||
|
from .simplified_configuration_manager import simplified_configuration_manager
|
||||||
|
|
||||||
|
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
|
||||||
|
|
||||||
|
if not signal:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Protocol signal {signal_id} not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"signal": signal.dict()
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting protocol signal {signal_id}: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get protocol signal: {str(e)}")
|
||||||
|
|
||||||
|
@dashboard_router.post("/protocol-signals")
|
||||||
|
async def create_protocol_signal(signal_data: dict):
|
||||||
|
"""Create a new protocol signal with simplified name + tags"""
|
||||||
|
try:
|
||||||
|
from .simplified_models import ProtocolSignalCreate, ProtocolType
|
||||||
|
from .simplified_configuration_manager import simplified_configuration_manager
|
||||||
|
|
||||||
|
# Convert protocol_type string to enum
|
||||||
|
if "protocol_type" not in signal_data:
|
||||||
|
raise HTTPException(status_code=400, detail="protocol_type is required")
|
||||||
|
|
||||||
|
try:
|
||||||
|
protocol_enum = ProtocolType(signal_data["protocol_type"])
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid protocol type: {signal_data['protocol_type']}")
|
||||||
|
|
||||||
|
# Create ProtocolSignalCreate object
|
||||||
|
signal_create = ProtocolSignalCreate(
|
||||||
|
signal_name=signal_data.get("signal_name"),
|
||||||
|
tags=signal_data.get("tags", []),
|
||||||
|
protocol_type=protocol_enum,
|
||||||
|
protocol_address=signal_data.get("protocol_address"),
|
||||||
|
db_source=signal_data.get("db_source"),
|
||||||
|
preprocessing_enabled=signal_data.get("preprocessing_enabled", False),
|
||||||
|
preprocessing_rules=signal_data.get("preprocessing_rules", []),
|
||||||
|
min_output_value=signal_data.get("min_output_value"),
|
||||||
|
max_output_value=signal_data.get("max_output_value"),
|
||||||
|
default_output_value=signal_data.get("default_output_value"),
|
||||||
|
modbus_config=signal_data.get("modbus_config"),
|
||||||
|
opcua_config=signal_data.get("opcua_config")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate configuration
|
||||||
|
validation = simplified_configuration_manager.validate_signal_configuration(signal_create)
|
||||||
|
if not validation["valid"]:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Configuration validation failed",
|
||||||
|
"errors": validation["errors"],
|
||||||
|
"warnings": validation["warnings"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the signal
|
||||||
|
success = simplified_configuration_manager.add_protocol_signal(signal_create)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Get the created signal to return
|
||||||
|
signal_id = signal_create.generate_signal_id()
|
||||||
|
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Protocol signal created successfully",
|
||||||
|
"signal": signal.dict() if signal else None,
|
||||||
|
"warnings": validation["warnings"]
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail="Failed to create protocol signal")
|
||||||
|
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.error(f"Validation error creating protocol signal: {str(e)}")
|
||||||
|
raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
|
||||||
|
except HTTPException:
|
||||||
|
# Re-raise HTTP exceptions
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating protocol signal: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to create protocol signal: {str(e)}")
|
||||||
|
|
||||||
|
@dashboard_router.put("/protocol-signals/{signal_id}")
|
||||||
|
async def update_protocol_signal(signal_id: str, signal_data: dict):
|
||||||
|
"""Update an existing protocol signal"""
|
||||||
|
try:
|
||||||
|
from .simplified_models import ProtocolSignalUpdate, ProtocolType
|
||||||
|
from .simplified_configuration_manager import simplified_configuration_manager
|
||||||
|
|
||||||
|
# Convert protocol_type string to enum if provided
|
||||||
|
protocol_enum = None
|
||||||
|
if "protocol_type" in signal_data:
|
||||||
|
try:
|
||||||
|
protocol_enum = ProtocolType(signal_data["protocol_type"])
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid protocol type: {signal_data['protocol_type']}")
|
||||||
|
|
||||||
|
# Create ProtocolSignalUpdate object
|
||||||
|
update_data = ProtocolSignalUpdate(
|
||||||
|
signal_name=signal_data.get("signal_name"),
|
||||||
|
tags=signal_data.get("tags"),
|
||||||
|
protocol_type=protocol_enum,
|
||||||
|
protocol_address=signal_data.get("protocol_address"),
|
||||||
|
db_source=signal_data.get("db_source"),
|
||||||
|
preprocessing_enabled=signal_data.get("preprocessing_enabled"),
|
||||||
|
preprocessing_rules=signal_data.get("preprocessing_rules"),
|
||||||
|
min_output_value=signal_data.get("min_output_value"),
|
||||||
|
max_output_value=signal_data.get("max_output_value"),
|
||||||
|
default_output_value=signal_data.get("default_output_value"),
|
||||||
|
modbus_config=signal_data.get("modbus_config"),
|
||||||
|
opcua_config=signal_data.get("opcua_config"),
|
||||||
|
enabled=signal_data.get("enabled")
|
||||||
|
)
|
||||||
|
|
||||||
|
success = simplified_configuration_manager.update_protocol_signal(signal_id, update_data)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Get the updated signal to return
|
||||||
|
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Protocol signal updated successfully",
|
||||||
|
"signal": signal.dict() if signal else None
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Protocol signal {signal_id} not found")
|
||||||
|
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.error(f"Validation error updating protocol signal: {str(e)}")
|
||||||
|
raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating protocol signal {signal_id}: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to update protocol signal: {str(e)}")
|
||||||
|
|
||||||
|
@dashboard_router.delete("/protocol-signals/{signal_id}")
|
||||||
|
async def delete_protocol_signal(signal_id: str):
|
||||||
|
"""Delete a protocol signal"""
|
||||||
|
try:
|
||||||
|
from .simplified_configuration_manager import simplified_configuration_manager
|
||||||
|
|
||||||
|
success = simplified_configuration_manager.delete_protocol_signal(signal_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Protocol signal {signal_id} deleted successfully"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Protocol signal {signal_id} not found")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting protocol signal {signal_id}: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to delete protocol signal: {str(e)}")
|
||||||
|
|
||||||
|
@dashboard_router.get("/protocol-signals/tags/all")
|
||||||
|
async def get_all_signal_tags():
|
||||||
|
"""Get all unique tags used across protocol signals"""
|
||||||
|
try:
|
||||||
|
from .simplified_configuration_manager import simplified_configuration_manager
|
||||||
|
|
||||||
|
all_tags = simplified_configuration_manager.get_all_tags()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"tags": all_tags,
|
||||||
|
"count": len(all_tags)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting all signal tags: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to get signal tags: {str(e)}")
|
||||||
|
|
||||||
# Tag-Based Metadata API Endpoints
|
# Tag-Based Metadata API Endpoints
|
||||||
|
|
||||||
@dashboard_router.get("/metadata/summary")
|
@dashboard_router.get("/metadata/summary")
|
||||||
|
|
|
||||||
|
|
@ -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 currentProtocolFilter = 'all';
|
||||||
let editingMappingId = null;
|
let editingSignalId = null;
|
||||||
let tagMetadata = {
|
let allTags = new Set();
|
||||||
stations: [],
|
|
||||||
equipment: [],
|
|
||||||
dataTypes: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tag Metadata Functions
|
// Simplified Signal Management Functions
|
||||||
async function loadTagMetadata() {
|
async function loadAllSignals() {
|
||||||
try {
|
try {
|
||||||
// Load stations
|
const response = await fetch('/api/v1/dashboard/protocol-signals');
|
||||||
const stationsResponse = await fetch('/api/v1/dashboard/metadata/stations');
|
|
||||||
const stationsData = await stationsResponse.json();
|
|
||||||
if (stationsData.success) {
|
|
||||||
tagMetadata.stations = stationsData.stations;
|
|
||||||
populateStationDropdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load data types
|
|
||||||
const dataTypesResponse = await fetch('/api/v1/dashboard/metadata/data-types');
|
|
||||||
const dataTypesData = await dataTypesResponse.json();
|
|
||||||
if (dataTypesData.success) {
|
|
||||||
tagMetadata.dataTypes = dataTypesData.data_types;
|
|
||||||
populateDataTypeDropdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load equipment for all stations
|
|
||||||
const equipmentResponse = await fetch('/api/v1/dashboard/metadata/equipment');
|
|
||||||
const equipmentData = await equipmentResponse.json();
|
|
||||||
if (equipmentData.success) {
|
|
||||||
tagMetadata.equipment = equipmentData.equipment;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading tag metadata:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateStationDropdown() {
|
|
||||||
const stationSelect = document.getElementById('station_id');
|
|
||||||
stationSelect.innerHTML = '<option value="">Select Station</option>';
|
|
||||||
|
|
||||||
tagMetadata.stations.forEach(station => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = station.id;
|
|
||||||
option.textContent = `${station.name} (${station.id})`;
|
|
||||||
stationSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateEquipmentDropdown(stationId = null) {
|
|
||||||
const equipmentSelect = document.getElementById('equipment_id');
|
|
||||||
equipmentSelect.innerHTML = '<option value="">Select Equipment</option>';
|
|
||||||
|
|
||||||
let filteredEquipment = tagMetadata.equipment;
|
|
||||||
if (stationId) {
|
|
||||||
filteredEquipment = tagMetadata.equipment.filter(eq => eq.station_id === stationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredEquipment.forEach(equipment => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = equipment.id;
|
|
||||||
option.textContent = `${equipment.name} (${equipment.id})`;
|
|
||||||
equipmentSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateDataTypeDropdown() {
|
|
||||||
const dataTypeSelect = document.getElementById('data_type_id');
|
|
||||||
dataTypeSelect.innerHTML = '<option value="">Select Data Type</option>';
|
|
||||||
|
|
||||||
tagMetadata.dataTypes.forEach(dataType => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = dataType.id;
|
|
||||||
option.textContent = `${dataType.name} (${dataType.id})`;
|
|
||||||
if (dataType.units) {
|
|
||||||
option.textContent += ` [${dataType.units}]`;
|
|
||||||
}
|
|
||||||
dataTypeSelect.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event listener for station selection change
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const stationSelect = document.getElementById('station_id');
|
|
||||||
if (stationSelect) {
|
|
||||||
stationSelect.addEventListener('change', function() {
|
|
||||||
const stationId = this.value;
|
|
||||||
populateEquipmentDropdown(stationId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load tag metadata when page loads
|
|
||||||
loadTagMetadata();
|
|
||||||
});
|
|
||||||
|
|
||||||
function selectProtocol(protocol) {
|
|
||||||
currentProtocolFilter = protocol;
|
|
||||||
|
|
||||||
// Update active button
|
|
||||||
document.querySelectorAll('.protocol-btn').forEach(btn => {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
});
|
|
||||||
event.target.classList.add('active');
|
|
||||||
|
|
||||||
// Reload mappings with filter
|
|
||||||
loadProtocolMappings();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadProtocolMappings() {
|
|
||||||
try {
|
|
||||||
// Ensure tag metadata is loaded first
|
|
||||||
if (tagMetadata.stations.length === 0 || tagMetadata.dataTypes.length === 0) {
|
|
||||||
await loadTagMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (currentProtocolFilter !== 'all') {
|
|
||||||
params.append('protocol_type', currentProtocolFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/dashboard/protocol-mappings?${params}`);
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
displayProtocolMappings(data.mappings);
|
displaySignals(data.signals);
|
||||||
|
updateTagCloud(data.signals);
|
||||||
} else {
|
} else {
|
||||||
showProtocolMappingAlert('Failed to load protocol mappings', 'error');
|
showSimplifiedAlert('Failed to load signals', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading protocol mappings:', error);
|
console.error('Error loading signals:', error);
|
||||||
showProtocolMappingAlert('Error loading protocol mappings', 'error');
|
showSimplifiedAlert('Error loading signals', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayProtocolMappings(mappings) {
|
function displaySignals(signals) {
|
||||||
const tbody = document.getElementById('protocol-mappings-body');
|
const tbody = document.getElementById('protocol-signals-body');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
if (mappings.length === 0) {
|
if (signals.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px;">No protocol mappings found</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 20px;">No protocol signals found</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mappings.forEach(mapping => {
|
signals.forEach(signal => {
|
||||||
// Look up human-readable names from tag metadata
|
|
||||||
const station = tagMetadata.stations.find(s => s.id === mapping.station_id);
|
|
||||||
const equipment = tagMetadata.equipment.find(e => e.id === mapping.equipment_id);
|
|
||||||
const dataType = tagMetadata.dataTypes.find(dt => dt.id === mapping.data_type_id);
|
|
||||||
|
|
||||||
const stationDisplay = station ? `${station.name} (${station.id})` : (mapping.station_id || '-');
|
|
||||||
const equipmentDisplay = equipment ? `${equipment.name} (${equipment.id})` : (mapping.equipment_id || '-');
|
|
||||||
const dataTypeDisplay = dataType ? `${dataType.name} (${dataType.id})` : (mapping.data_type_id || '-');
|
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.id}</td>
|
<td style="padding: 10px; border: 1px solid #ddd;">${signal.signal_name}</td>
|
||||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.protocol_type}</td>
|
<td style="padding: 10px; border: 1px solid #ddd;">${signal.protocol_type}</td>
|
||||||
<td style="padding: 10px; border: 1px solid #ddd;">${stationDisplay}</td>
|
|
||||||
<td style="padding: 10px; border: 1px solid #ddd;">${equipmentDisplay}</td>
|
|
||||||
<td style="padding: 10px; border: 1px solid #ddd;">${dataTypeDisplay}</td>
|
|
||||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.protocol_address}</td>
|
|
||||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.db_source}</td>
|
|
||||||
<td style="padding: 10px; border: 1px solid #ddd;">
|
<td style="padding: 10px; border: 1px solid #ddd;">
|
||||||
<button onclick="editMapping('${mapping.id}')" style="background: #007acc; margin-right: 5px;">Edit</button>
|
${signal.tags.map(tag => `<span class="tag-badge">${tag}</span>`).join('')}
|
||||||
<button onclick="deleteMapping('${mapping.id}')" style="background: #dc3545;">Delete</button>
|
</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid #ddd;">${signal.protocol_address}</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid #ddd;">${signal.db_source}</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid #ddd;">
|
||||||
|
<span class="status-badge ${signal.enabled ? 'enabled' : 'disabled'}">
|
||||||
|
${signal.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px; border: 1px solid #ddd;">
|
||||||
|
<button onclick="editSignal('${signal.signal_id}')" class="btn-edit">Edit</button>
|
||||||
|
<button onclick="deleteSignal('${signal.signal_id}')" class="btn-delete">Delete</button>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAddMappingModal() {
|
function updateTagCloud(signals) {
|
||||||
editingMappingId = null;
|
const tagCloud = document.getElementById('tag-cloud');
|
||||||
document.getElementById('modal-title').textContent = 'Add Protocol Mapping';
|
if (!tagCloud) return;
|
||||||
document.getElementById('mapping-form').reset();
|
|
||||||
document.getElementById('protocol_address_help').textContent = '';
|
// Collect all tags
|
||||||
document.getElementById('mapping-modal').style.display = 'block';
|
const tagCounts = {};
|
||||||
|
signals.forEach(signal => {
|
||||||
|
signal.tags.forEach(tag => {
|
||||||
|
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create tag cloud
|
||||||
|
tagCloud.innerHTML = '';
|
||||||
|
Object.entries(tagCounts).forEach(([tag, count]) => {
|
||||||
|
const tagElement = document.createElement('span');
|
||||||
|
tagElement.className = 'tag-cloud-item';
|
||||||
|
tagElement.textContent = tag;
|
||||||
|
tagElement.title = `${count} signal(s)`;
|
||||||
|
tagElement.onclick = () => filterByTag(tag);
|
||||||
|
tagCloud.appendChild(tagElement);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showEditMappingModal(mapping) {
|
function filterByTag(tag) {
|
||||||
editingMappingId = mapping.id;
|
const filterInput = document.getElementById('tag-filter');
|
||||||
document.getElementById('modal-title').textContent = 'Edit Protocol Mapping';
|
if (filterInput) {
|
||||||
document.getElementById('mapping_id').value = mapping.id;
|
filterInput.value = tag;
|
||||||
document.getElementById('protocol_type').value = mapping.protocol_type;
|
applyFilters();
|
||||||
|
}
|
||||||
// Set dropdown values
|
|
||||||
const stationSelect = document.getElementById('station_id');
|
|
||||||
const equipmentSelect = document.getElementById('equipment_id');
|
|
||||||
const dataTypeSelect = document.getElementById('data_type_id');
|
|
||||||
|
|
||||||
stationSelect.value = mapping.station_id || '';
|
|
||||||
if (mapping.station_id) {
|
|
||||||
populateEquipmentDropdown(mapping.station_id);
|
|
||||||
}
|
}
|
||||||
equipmentSelect.value = mapping.equipment_id || '';
|
|
||||||
dataTypeSelect.value = mapping.data_type_id || '';
|
|
||||||
|
|
||||||
document.getElementById('protocol_address').value = mapping.protocol_address;
|
async function applyFilters() {
|
||||||
document.getElementById('db_source').value = mapping.db_source;
|
const tagFilter = document.getElementById('tag-filter')?.value || '';
|
||||||
|
const protocolFilter = document.getElementById('protocol-filter')?.value || 'all';
|
||||||
|
const nameFilter = document.getElementById('name-filter')?.value || '';
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (tagFilter) params.append('tags', tagFilter);
|
||||||
|
if (protocolFilter !== 'all') params.append('protocol_type', protocolFilter);
|
||||||
|
if (nameFilter) params.append('signal_name_contains', nameFilter);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/dashboard/protocol-signals?${params}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
displaySignals(data.signals);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying filters:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Functions
|
||||||
|
function showAddSignalModal() {
|
||||||
|
editingSignalId = null;
|
||||||
|
document.getElementById('modal-title').textContent = 'Add Protocol Signal';
|
||||||
|
document.getElementById('signal-form').reset();
|
||||||
|
document.getElementById('protocol-address-help').textContent = '';
|
||||||
|
document.getElementById('signal-modal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditSignalModal(signal) {
|
||||||
|
editingSignalId = signal.signal_id;
|
||||||
|
document.getElementById('modal-title').textContent = 'Edit Protocol Signal';
|
||||||
|
|
||||||
|
// Populate form
|
||||||
|
document.getElementById('signal_name').value = signal.signal_name;
|
||||||
|
document.getElementById('tags').value = signal.tags.join(', ');
|
||||||
|
document.getElementById('protocol_type').value = signal.protocol_type;
|
||||||
|
document.getElementById('protocol_address').value = signal.protocol_address;
|
||||||
|
document.getElementById('db_source').value = signal.db_source;
|
||||||
|
document.getElementById('preprocessing_enabled').checked = signal.preprocessing_enabled || false;
|
||||||
|
|
||||||
updateProtocolFields();
|
updateProtocolFields();
|
||||||
document.getElementById('mapping-modal').style.display = 'block';
|
document.getElementById('signal-modal').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeMappingModal() {
|
function closeSignalModal() {
|
||||||
document.getElementById('mapping-modal').style.display = 'none';
|
document.getElementById('signal-modal').style.display = 'none';
|
||||||
editingMappingId = null;
|
editingSignalId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProtocolFields() {
|
function updateProtocolFields() {
|
||||||
const protocolType = document.getElementById('protocol_type').value;
|
const protocolType = document.getElementById('protocol_type').value;
|
||||||
const helpText = document.getElementById('protocol_address_help');
|
const helpText = document.getElementById('protocol-address-help');
|
||||||
|
|
||||||
switch (protocolType) {
|
switch (protocolType) {
|
||||||
case 'modbus_tcp':
|
case 'modbus_tcp':
|
||||||
|
case 'modbus_rtu':
|
||||||
helpText.textContent = 'Modbus address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
|
helpText.textContent = 'Modbus address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
|
||||||
break;
|
break;
|
||||||
case 'opcua':
|
case 'opcua':
|
||||||
helpText.textContent = 'OPC UA NodeId format: ns=2;s=MyVariable or ns=2;i=1234';
|
helpText.textContent = 'OPC UA NodeId format: ns=2;s=MyVariable or ns=2;i=1234';
|
||||||
break;
|
break;
|
||||||
case 'modbus_rtu':
|
|
||||||
helpText.textContent = 'Modbus RTU address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
|
|
||||||
break;
|
|
||||||
case 'rest_api':
|
case 'rest_api':
|
||||||
helpText.textContent = 'REST API endpoint format: /api/v1/data/endpoint';
|
helpText.textContent = 'REST API endpoint format: /api/v1/data/endpoint';
|
||||||
break;
|
break;
|
||||||
|
|
@ -232,48 +160,22 @@ function updateProtocolFields() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateMapping() {
|
// Form Submission
|
||||||
const formData = getMappingFormData();
|
async function saveSignal(event) {
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/v1/dashboard/protocol-mappings/${editingMappingId || 'new'}/validate`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(formData)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
if (data.valid) {
|
|
||||||
showProtocolMappingAlert('Mapping validation successful!', 'success');
|
|
||||||
} else {
|
|
||||||
showProtocolMappingAlert(`Validation failed: ${data.errors.join(', ')}`, 'error');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showProtocolMappingAlert('Validation error', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error validating mapping:', error);
|
|
||||||
showProtocolMappingAlert('Error validating mapping', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveMapping(event) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const formData = getMappingFormData();
|
const formData = getSignalFormData();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
if (editingMappingId) {
|
if (editingSignalId) {
|
||||||
response = await fetch(`/api/v1/dashboard/protocol-mappings/${editingMappingId}`, {
|
response = await fetch(`/api/v1/dashboard/protocol-signals/${editingSignalId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(formData)
|
body: JSON.stringify(formData)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
response = await fetch('/api/v1/dashboard/protocol-mappings', {
|
response = await fetch('/api/v1/dashboard/protocol-signals', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(formData)
|
body: JSON.stringify(formData)
|
||||||
|
|
@ -283,76 +185,151 @@ async function saveMapping(event) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showProtocolMappingAlert(`Protocol mapping ${editingMappingId ? 'updated' : 'created'} successfully!`, 'success');
|
showSimplifiedAlert(`Protocol signal ${editingSignalId ? 'updated' : 'created'} successfully!`, 'success');
|
||||||
closeMappingModal();
|
closeSignalModal();
|
||||||
loadProtocolMappings();
|
loadAllSignals();
|
||||||
} else {
|
} else {
|
||||||
showProtocolMappingAlert(`Failed to save mapping: ${data.detail || 'Unknown error'}`, 'error');
|
showSimplifiedAlert(`Failed to save signal: ${data.detail || 'Unknown error'}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving mapping:', error);
|
console.error('Error saving signal:', error);
|
||||||
showProtocolMappingAlert('Error saving mapping', 'error');
|
showSimplifiedAlert('Error saving signal', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMappingFormData() {
|
function getSignalFormData() {
|
||||||
|
const tagsInput = document.getElementById('tags').value;
|
||||||
|
const tags = tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
signal_name: document.getElementById('signal_name').value,
|
||||||
|
tags: tags,
|
||||||
protocol_type: document.getElementById('protocol_type').value,
|
protocol_type: document.getElementById('protocol_type').value,
|
||||||
station_id: document.getElementById('station_id').value,
|
|
||||||
equipment_id: document.getElementById('equipment_id').value,
|
|
||||||
data_type_id: document.getElementById('data_type_id').value,
|
|
||||||
protocol_address: document.getElementById('protocol_address').value,
|
protocol_address: document.getElementById('protocol_address').value,
|
||||||
db_source: document.getElementById('db_source').value
|
db_source: document.getElementById('db_source').value,
|
||||||
|
preprocessing_enabled: document.getElementById('preprocessing_enabled').checked
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editMapping(mappingId) {
|
// Signal Management
|
||||||
|
async function editSignal(signalId) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/dashboard/protocol-mappings?protocol_type=all`);
|
const response = await fetch(`/api/v1/dashboard/protocol-signals/${signalId}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const mapping = data.mappings.find(m => m.id === mappingId);
|
showEditSignalModal(data.signal);
|
||||||
if (mapping) {
|
|
||||||
showEditMappingModal(mapping);
|
|
||||||
} else {
|
} else {
|
||||||
showProtocolMappingAlert('Mapping not found', 'error');
|
showSimplifiedAlert('Signal not found', 'error');
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showProtocolMappingAlert('Failed to load mapping', 'error');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading mapping:', error);
|
console.error('Error loading signal:', error);
|
||||||
showProtocolMappingAlert('Error loading mapping', 'error');
|
showSimplifiedAlert('Error loading signal', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMapping(mappingId) {
|
async function deleteSignal(signalId) {
|
||||||
if (!confirm(`Are you sure you want to delete mapping ${mappingId}?`)) {
|
if (!confirm('Are you sure you want to delete this signal?')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/v1/dashboard/protocol-mappings/${mappingId}`, {
|
const response = await fetch(`/api/v1/dashboard/protocol-signals/${signalId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showProtocolMappingAlert('Mapping deleted successfully!', 'success');
|
showSimplifiedAlert('Signal deleted successfully!', 'success');
|
||||||
loadProtocolMappings();
|
loadAllSignals();
|
||||||
} else {
|
} else {
|
||||||
showProtocolMappingAlert(`Failed to delete mapping: ${data.detail || 'Unknown error'}`, 'error');
|
showSimplifiedAlert(`Failed to delete signal: ${data.detail || 'Unknown error'}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting mapping:', error);
|
console.error('Error deleting signal:', error);
|
||||||
showProtocolMappingAlert('Error deleting mapping', 'error');
|
showSimplifiedAlert('Error deleting signal', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showProtocolMappingAlert(message, type) {
|
// Discovery Integration
|
||||||
const alertsDiv = document.getElementById('protocol-mapping-alerts');
|
function autoPopulateSignalForm(discoveryData) {
|
||||||
|
console.log('Auto-populating signal form with:', discoveryData);
|
||||||
|
|
||||||
|
// First, open the "Add New Signal" modal
|
||||||
|
showAddSignalModal();
|
||||||
|
|
||||||
|
// Wait for modal to be fully loaded and visible
|
||||||
|
const waitForModal = setInterval(() => {
|
||||||
|
const modal = document.getElementById('signal-modal');
|
||||||
|
const isModalVisible = modal && modal.style.display !== 'none';
|
||||||
|
|
||||||
|
if (isModalVisible) {
|
||||||
|
clearInterval(waitForModal);
|
||||||
|
populateModalFields(discoveryData);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
// Timeout after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(waitForModal);
|
||||||
|
const modal = document.getElementById('signal-modal');
|
||||||
|
if (modal && modal.style.display !== 'none') {
|
||||||
|
populateModalFields(discoveryData);
|
||||||
|
} else {
|
||||||
|
console.error('Modal did not open within timeout period');
|
||||||
|
showSimplifiedAlert('Could not open signal form. Please try opening it manually.', 'error');
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateModalFields(discoveryData) {
|
||||||
|
console.log('Populating modal fields with:', discoveryData);
|
||||||
|
|
||||||
|
// Populate signal name
|
||||||
|
const signalNameField = document.getElementById('signal_name');
|
||||||
|
if (signalNameField && discoveryData.signal_name) {
|
||||||
|
signalNameField.value = discoveryData.signal_name;
|
||||||
|
console.log('✓ Set signal_name to:', discoveryData.signal_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate tags
|
||||||
|
const tagsField = document.getElementById('tags');
|
||||||
|
if (tagsField && discoveryData.tags) {
|
||||||
|
tagsField.value = discoveryData.tags.join(', ');
|
||||||
|
console.log('✓ Set tags to:', discoveryData.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate protocol type
|
||||||
|
const protocolTypeField = document.getElementById('protocol_type');
|
||||||
|
if (protocolTypeField && discoveryData.protocol_type) {
|
||||||
|
protocolTypeField.value = discoveryData.protocol_type;
|
||||||
|
console.log('✓ Set protocol_type to:', discoveryData.protocol_type);
|
||||||
|
// Trigger protocol field updates
|
||||||
|
protocolTypeField.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate protocol address
|
||||||
|
const protocolAddressField = document.getElementById('protocol_address');
|
||||||
|
if (protocolAddressField && discoveryData.protocol_address) {
|
||||||
|
protocolAddressField.value = discoveryData.protocol_address;
|
||||||
|
console.log('✓ Set protocol_address to:', discoveryData.protocol_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate database source
|
||||||
|
const dbSourceField = document.getElementById('db_source');
|
||||||
|
if (dbSourceField && discoveryData.db_source) {
|
||||||
|
dbSourceField.value = discoveryData.db_source;
|
||||||
|
console.log('✓ Set db_source to:', discoveryData.db_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
showSimplifiedAlert(`Signal form populated with discovery data. Please review and save.`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility Functions
|
||||||
|
function showSimplifiedAlert(message, type = 'info') {
|
||||||
|
const alertsDiv = document.getElementById('simplified-alerts');
|
||||||
const alertDiv = document.createElement('div');
|
const alertDiv = document.createElement('div');
|
||||||
alertDiv.className = `alert ${type === 'error' ? 'error' : 'success'}`;
|
alertDiv.className = `alert ${type === 'error' ? 'error' : 'success'}`;
|
||||||
alertDiv.textContent = message;
|
alertDiv.textContent = message;
|
||||||
|
|
@ -360,57 +337,21 @@ function showProtocolMappingAlert(message, type) {
|
||||||
alertsDiv.innerHTML = '';
|
alertsDiv.innerHTML = '';
|
||||||
alertsDiv.appendChild(alertDiv);
|
alertsDiv.appendChild(alertDiv);
|
||||||
|
|
||||||
|
// Auto-remove after 5 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (alertDiv.parentNode) {
|
||||||
alertDiv.remove();
|
alertDiv.remove();
|
||||||
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportProtocolMappings() {
|
// Initialize
|
||||||
try {
|
|
||||||
const response = await fetch('/api/v1/dashboard/protocol-mappings?protocol_type=all');
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
const csvContent = convertToCSV(data.mappings);
|
|
||||||
downloadCSV(csvContent, 'protocol_mappings.csv');
|
|
||||||
} else {
|
|
||||||
showProtocolMappingAlert('Failed to export mappings', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting mappings:', error);
|
|
||||||
showProtocolMappingAlert('Error exporting mappings', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToCSV(mappings) {
|
|
||||||
const headers = ['ID', 'Protocol', 'Station', 'Pump', 'Data Type', 'Protocol Address', 'Database Source'];
|
|
||||||
const rows = mappings.map(mapping => [
|
|
||||||
mapping.id,
|
|
||||||
mapping.protocol_type,
|
|
||||||
mapping.station_id || '',
|
|
||||||
mapping.pump_id || '',
|
|
||||||
mapping.data_type,
|
|
||||||
mapping.protocol_address,
|
|
||||||
mapping.db_source
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [headers, ...rows].map(row => row.map(field => `"${field}"`).join(',')).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadCSV(content, filename) {
|
|
||||||
const blob = new Blob([content], { type: 'text/csv' });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
a.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize form submission handler
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const mappingForm = document.getElementById('mapping-form');
|
const signalForm = document.getElementById('signal-form');
|
||||||
if (mappingForm) {
|
if (signalForm) {
|
||||||
mappingForm.addEventListener('submit', saveMapping);
|
signalForm.addEventListener('submit', saveSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
loadAllSignals();
|
||||||
});
|
});
|
||||||
|
|
@ -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