Compare commits
No commits in common. "f0d6aca5ed5bea1c0511fbf1bc56aa6ea3a05d7f" and "c741ac8553c792ae6a28c6ca8012eda49a29333c" have entirely different histories.
f0d6aca5ed
...
c741ac8553
|
|
@ -1,97 +0,0 @@
|
|||
# Legacy System Removal Summary
|
||||
|
||||
## Overview
|
||||
Successfully removed the legacy station/pump configuration system and fully integrated the tag-based metadata system throughout the Calejo Control application.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Configuration Manager (`src/dashboard/configuration_manager.py`)
|
||||
- **Removed legacy classes**: `PumpStationConfig`, `PumpConfig`, `SafetyLimitsConfig`
|
||||
- **Updated `ProtocolMapping` model**: Added validators to check `station_id`, `equipment_id`, and `data_type_id` against the tag metadata system
|
||||
- **Updated `HardwareDiscoveryResult`**: Changed from legacy class references to generic dictionaries
|
||||
- **Cleaned up configuration methods**: Removed legacy configuration export/import methods
|
||||
|
||||
### 2. API Endpoints (`src/dashboard/api.py`)
|
||||
- **Removed legacy endpoints**: `/configure/station`, `/configure/pump`, `/configure/safety-limits`
|
||||
- **Added tag metadata endpoints**: `/metadata/stations`, `/metadata/equipment`, `/metadata/data-types`
|
||||
- **Updated protocol mapping endpoints**: Now validate against tag metadata system
|
||||
|
||||
### 3. UI Templates (`src/dashboard/templates.py`)
|
||||
- **Replaced text inputs with dropdowns**: For `station_id`, `equipment_id`, and `data_type_id` fields
|
||||
- **Added dynamic loading**: Dropdowns are populated from tag metadata API endpoints
|
||||
- **Updated form validation**: Now validates against available tag metadata
|
||||
- **Enhanced table display**: Shows human-readable names with IDs in protocol mappings table
|
||||
- **Updated headers**: Descriptive column headers indicate "Name & ID" format
|
||||
|
||||
### 4. JavaScript (`static/protocol_mapping.js`)
|
||||
- **Added tag metadata loading functions**: `loadTagMetadata()`, `populateStationDropdown()`, `populateEquipmentDropdown()`, `populateDataTypeDropdown()`
|
||||
- **Updated form handling**: Now validates against tag metadata before submission
|
||||
- **Enhanced user experience**: Dropdowns provide selection from available tag metadata
|
||||
- **Improved table display**: `displayProtocolMappings` shows human-readable names from tag metadata
|
||||
- **Ensured metadata loading**: `loadProtocolMappings` ensures tag metadata is loaded before display
|
||||
|
||||
### 5. Security Module (`src/core/security.py`)
|
||||
- **Removed legacy permissions**: `configure_safety_limits` permission removed from ENGINEER and ADMINISTRATOR roles
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Validation System
|
||||
- **Station Validation**: `station_id` must exist in tag metadata stations
|
||||
- **Equipment Validation**: `equipment_id` must exist in tag metadata equipment
|
||||
- **Data Type Validation**: `data_type_id` must exist in tag metadata data types
|
||||
|
||||
### API Integration
|
||||
- **Metadata Endpoints**: Provide real-time access to tag metadata
|
||||
- **Protocol Mapping**: All mappings now reference tag metadata IDs
|
||||
- **Error Handling**: Clear validation errors when tag metadata doesn't exist
|
||||
|
||||
### User Interface
|
||||
- **Dropdown Selection**: Users select from available tag metadata instead of manual entry
|
||||
- **Dynamic Loading**: Dropdowns populated from API endpoints on page load
|
||||
- **Validation Feedback**: Clear error messages when invalid selections are made
|
||||
- **Human-Readable Display**: Protocol mappings table shows descriptive names with IDs
|
||||
- **Enhanced Usability**: Users can easily identify stations, equipment, and data types by name
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Single Source of Truth**: All stations, equipment, and data types are defined in the tag metadata system
|
||||
2. **Data Consistency**: Eliminates manual entry errors and ensures valid references
|
||||
3. **Improved User Experience**: Dropdown selection is faster and more reliable than manual entry
|
||||
4. **System Integrity**: Validators prevent invalid configurations from being saved
|
||||
5. **Maintainability**: Simplified codebase with unified metadata approach
|
||||
6. **Human-Readable Display**: UI shows descriptive names instead of raw IDs for better user experience
|
||||
|
||||
## Sample Metadata
|
||||
|
||||
The system includes sample metadata for demonstration:
|
||||
|
||||
### Stations
|
||||
- **Main Pump Station** (`station_main`) - Primary water pumping station
|
||||
- **Backup Pump Station** (`station_backup`) - Emergency backup pumping station
|
||||
|
||||
### Equipment
|
||||
- **Primary Pump** (`pump_primary`) - Main water pump with variable speed drive
|
||||
- **Backup Pump** (`pump_backup`) - Emergency backup water pump
|
||||
- **Pressure Sensor** (`sensor_pressure`) - Water pressure monitoring sensor
|
||||
- **Flow Meter** (`sensor_flow`) - Water flow rate measurement device
|
||||
|
||||
### Data Types
|
||||
- **Pump Speed** (`speed_pump`) - Pump motor speed control (RPM, 0-3000)
|
||||
- **Water Pressure** (`pressure_water`) - Water pressure measurement (PSI, 0-100)
|
||||
- **Pump Status** (`status_pump`) - Pump operational status
|
||||
- **Flow Rate** (`flow_rate`) - Water flow rate measurement (GPM, 0-1000)
|
||||
|
||||
## Testing
|
||||
|
||||
All integration tests passed:
|
||||
- ✅ Configuration manager imports without legacy classes
|
||||
- ✅ ProtocolMapping validators check against tag metadata system
|
||||
- ✅ API endpoints use tag metadata system
|
||||
- ✅ UI templates use dropdowns instead of text inputs
|
||||
- ✅ Legacy endpoints and classes completely removed
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Existing protocol mappings will need to be updated to use valid tag metadata IDs
|
||||
- Tag metadata must be populated before creating new protocol mappings
|
||||
- The system now requires all stations, equipment, and data types to be defined in the tag metadata system before use
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
-- 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
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to initialize and persist sample tag metadata
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
# Add the src directory to the Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from src.core.tag_metadata_manager import tag_metadata_manager
|
||||
|
||||
def create_and_save_sample_metadata():
|
||||
"""Create sample tag metadata and save to file"""
|
||||
|
||||
print("Initializing Sample Tag Metadata...")
|
||||
print("=" * 60)
|
||||
|
||||
# Create sample stations
|
||||
print("\n🏭 Creating Stations...")
|
||||
station1_id = tag_metadata_manager.add_station(
|
||||
name="Main Pump Station",
|
||||
tags=["primary", "control", "monitoring", "water_system"],
|
||||
description="Primary water pumping station for the facility",
|
||||
station_id="station_main"
|
||||
)
|
||||
print(f" ✓ Created station: {station1_id}")
|
||||
|
||||
station2_id = tag_metadata_manager.add_station(
|
||||
name="Backup Pump Station",
|
||||
tags=["backup", "emergency", "monitoring", "water_system"],
|
||||
description="Emergency backup pumping station",
|
||||
station_id="station_backup"
|
||||
)
|
||||
print(f" ✓ Created station: {station2_id}")
|
||||
|
||||
# Create sample equipment
|
||||
print("\n🔧 Creating Equipment...")
|
||||
equipment1_id = tag_metadata_manager.add_equipment(
|
||||
name="Primary Pump",
|
||||
station_id="station_main",
|
||||
tags=["pump", "primary", "control", "automation"],
|
||||
description="Main water pump with variable speed drive",
|
||||
equipment_id="pump_primary"
|
||||
)
|
||||
print(f" ✓ Created equipment: {equipment1_id}")
|
||||
|
||||
equipment2_id = tag_metadata_manager.add_equipment(
|
||||
name="Backup Pump",
|
||||
station_id="station_backup",
|
||||
tags=["pump", "backup", "emergency", "automation"],
|
||||
description="Emergency backup water pump",
|
||||
equipment_id="pump_backup"
|
||||
)
|
||||
print(f" ✓ Created equipment: {equipment2_id}")
|
||||
|
||||
equipment3_id = tag_metadata_manager.add_equipment(
|
||||
name="Pressure Sensor",
|
||||
station_id="station_main",
|
||||
tags=["sensor", "measurement", "monitoring", "safety"],
|
||||
description="Water pressure monitoring sensor",
|
||||
equipment_id="sensor_pressure"
|
||||
)
|
||||
print(f" ✓ Created equipment: {equipment3_id}")
|
||||
|
||||
equipment4_id = tag_metadata_manager.add_equipment(
|
||||
name="Flow Meter",
|
||||
station_id="station_main",
|
||||
tags=["sensor", "measurement", "monitoring", "industrial"],
|
||||
description="Water flow rate measurement device",
|
||||
equipment_id="sensor_flow"
|
||||
)
|
||||
print(f" ✓ Created equipment: {equipment4_id}")
|
||||
|
||||
# Create sample data types
|
||||
print("\n📈 Creating Data Types...")
|
||||
data_type1_id = tag_metadata_manager.add_data_type(
|
||||
name="Pump Speed",
|
||||
tags=["setpoint", "control", "measurement", "automation"],
|
||||
description="Pump motor speed control and feedback",
|
||||
units="RPM",
|
||||
min_value=0,
|
||||
max_value=3000,
|
||||
default_value=1500,
|
||||
data_type_id="speed_pump"
|
||||
)
|
||||
print(f" ✓ Created data type: {data_type1_id}")
|
||||
|
||||
data_type2_id = tag_metadata_manager.add_data_type(
|
||||
name="Water Pressure",
|
||||
tags=["measurement", "monitoring", "alarm", "safety"],
|
||||
description="Water pressure measurement",
|
||||
units="PSI",
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
default_value=50,
|
||||
data_type_id="pressure_water"
|
||||
)
|
||||
print(f" ✓ Created data type: {data_type2_id}")
|
||||
|
||||
data_type3_id = tag_metadata_manager.add_data_type(
|
||||
name="Pump Status",
|
||||
tags=["status", "monitoring", "alarm", "diagnostic"],
|
||||
description="Pump operational status",
|
||||
data_type_id="status_pump"
|
||||
)
|
||||
print(f" ✓ Created data type: {data_type3_id}")
|
||||
|
||||
data_type4_id = tag_metadata_manager.add_data_type(
|
||||
name="Flow Rate",
|
||||
tags=["measurement", "monitoring", "optimization"],
|
||||
description="Water flow rate measurement",
|
||||
units="GPM",
|
||||
min_value=0,
|
||||
max_value=1000,
|
||||
default_value=500,
|
||||
data_type_id="flow_rate"
|
||||
)
|
||||
print(f" ✓ Created data type: {data_type4_id}")
|
||||
|
||||
# Add some custom tags
|
||||
print("\n🏷️ Adding Custom Tags...")
|
||||
custom_tags = ["water_system", "industrial", "automation", "safety", "municipal"]
|
||||
for tag in custom_tags:
|
||||
tag_metadata_manager.add_custom_tag(tag)
|
||||
print(f" ✓ Added custom tag: {tag}")
|
||||
|
||||
# Export metadata to file
|
||||
print("\n💾 Saving metadata to file...")
|
||||
metadata_file = os.path.join(os.path.dirname(__file__), 'sample_metadata.json')
|
||||
metadata = tag_metadata_manager.export_metadata()
|
||||
|
||||
with open(metadata_file, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
print(f" ✓ Metadata saved to: {metadata_file}")
|
||||
|
||||
# Show summary
|
||||
print("\n📋 FINAL SUMMARY:")
|
||||
print("-" * 40)
|
||||
print(f" Stations: {len(tag_metadata_manager.stations)}")
|
||||
print(f" Equipment: {len(tag_metadata_manager.equipment)}")
|
||||
print(f" Data Types: {len(tag_metadata_manager.data_types)}")
|
||||
print(f" Total Tags: {len(tag_metadata_manager.all_tags)}")
|
||||
|
||||
print("\n✅ Sample metadata initialization completed!")
|
||||
print("\n📝 Sample metadata includes:")
|
||||
print(" - 2 Stations: Main Pump Station, Backup Pump Station")
|
||||
print(" - 4 Equipment: Primary Pump, Backup Pump, Pressure Sensor, Flow Meter")
|
||||
print(" - 4 Data Types: Pump Speed, Water Pressure, Pump Status, Flow Rate")
|
||||
print(" - 33 Total Tags including core and custom tags")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_and_save_sample_metadata()
|
||||
|
|
@ -1,251 +0,0 @@
|
|||
{
|
||||
"stations": {
|
||||
"station_main": {
|
||||
"id": "station_main",
|
||||
"name": "Main Pump Station",
|
||||
"tags": [
|
||||
"primary",
|
||||
"control",
|
||||
"monitoring",
|
||||
"water_system"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Primary water pumping station for the facility"
|
||||
},
|
||||
"station_backup": {
|
||||
"id": "station_backup",
|
||||
"name": "Backup Pump Station",
|
||||
"tags": [
|
||||
"backup",
|
||||
"emergency",
|
||||
"monitoring",
|
||||
"water_system"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Emergency backup pumping station"
|
||||
},
|
||||
"station_control": {
|
||||
"id": "station_control",
|
||||
"name": "Control Station",
|
||||
"tags": [
|
||||
"local",
|
||||
"control",
|
||||
"automation",
|
||||
"water_system"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Main control and monitoring station"
|
||||
}
|
||||
},
|
||||
"equipment": {
|
||||
"pump_primary": {
|
||||
"id": "pump_primary",
|
||||
"name": "Primary Pump",
|
||||
"tags": [
|
||||
"pump",
|
||||
"primary",
|
||||
"control",
|
||||
"automation"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Main water pump with variable speed drive",
|
||||
"station_id": "station_main"
|
||||
},
|
||||
"pump_backup": {
|
||||
"id": "pump_backup",
|
||||
"name": "Backup Pump",
|
||||
"tags": [
|
||||
"pump",
|
||||
"backup",
|
||||
"emergency",
|
||||
"automation"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Emergency backup water pump",
|
||||
"station_id": "station_backup"
|
||||
},
|
||||
"sensor_pressure": {
|
||||
"id": "sensor_pressure",
|
||||
"name": "Pressure Sensor",
|
||||
"tags": [
|
||||
"sensor",
|
||||
"measurement",
|
||||
"monitoring",
|
||||
"safety"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Water pressure monitoring sensor",
|
||||
"station_id": "station_main"
|
||||
},
|
||||
"sensor_flow": {
|
||||
"id": "sensor_flow",
|
||||
"name": "Flow Meter",
|
||||
"tags": [
|
||||
"sensor",
|
||||
"measurement",
|
||||
"monitoring",
|
||||
"industrial"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Water flow rate measurement device",
|
||||
"station_id": "station_main"
|
||||
},
|
||||
"valve_control": {
|
||||
"id": "valve_control",
|
||||
"name": "Control Valve",
|
||||
"tags": [
|
||||
"valve",
|
||||
"control",
|
||||
"automation",
|
||||
"safety"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Flow control valve with position feedback",
|
||||
"station_id": "station_main"
|
||||
},
|
||||
"controller_plc": {
|
||||
"id": "controller_plc",
|
||||
"name": "PLC Controller",
|
||||
"tags": [
|
||||
"controller",
|
||||
"automation",
|
||||
"control",
|
||||
"industrial"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Programmable Logic Controller for system automation",
|
||||
"station_id": "station_control"
|
||||
}
|
||||
},
|
||||
"data_types": {
|
||||
"speed_pump": {
|
||||
"id": "speed_pump",
|
||||
"name": "Pump Speed",
|
||||
"tags": [
|
||||
"setpoint",
|
||||
"control",
|
||||
"measurement",
|
||||
"automation"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Pump motor speed control and feedback",
|
||||
"units": "RPM",
|
||||
"min_value": 0,
|
||||
"max_value": 3000,
|
||||
"default_value": 1500
|
||||
},
|
||||
"pressure_water": {
|
||||
"id": "pressure_water",
|
||||
"name": "Water Pressure",
|
||||
"tags": [
|
||||
"measurement",
|
||||
"monitoring",
|
||||
"alarm",
|
||||
"safety"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Water pressure measurement",
|
||||
"units": "PSI",
|
||||
"min_value": 0,
|
||||
"max_value": 100,
|
||||
"default_value": 50
|
||||
},
|
||||
"status_pump": {
|
||||
"id": "status_pump",
|
||||
"name": "Pump Status",
|
||||
"tags": [
|
||||
"status",
|
||||
"monitoring",
|
||||
"alarm",
|
||||
"diagnostic"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Pump operational status",
|
||||
"units": null,
|
||||
"min_value": null,
|
||||
"max_value": null,
|
||||
"default_value": null
|
||||
},
|
||||
"flow_rate": {
|
||||
"id": "flow_rate",
|
||||
"name": "Flow Rate",
|
||||
"tags": [
|
||||
"measurement",
|
||||
"monitoring",
|
||||
"optimization"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Water flow rate measurement",
|
||||
"units": "GPM",
|
||||
"min_value": 0,
|
||||
"max_value": 1000,
|
||||
"default_value": 500
|
||||
},
|
||||
"position_valve": {
|
||||
"id": "position_valve",
|
||||
"name": "Valve Position",
|
||||
"tags": [
|
||||
"setpoint",
|
||||
"feedback",
|
||||
"control",
|
||||
"automation"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Control valve position command and feedback",
|
||||
"units": "%",
|
||||
"min_value": 0,
|
||||
"max_value": 100,
|
||||
"default_value": 0
|
||||
},
|
||||
"emergency_stop": {
|
||||
"id": "emergency_stop",
|
||||
"name": "Emergency Stop",
|
||||
"tags": [
|
||||
"command",
|
||||
"safety",
|
||||
"alarm",
|
||||
"emergency"
|
||||
],
|
||||
"attributes": {},
|
||||
"description": "Emergency stop command and status",
|
||||
"units": null,
|
||||
"min_value": null,
|
||||
"max_value": null,
|
||||
"default_value": null
|
||||
}
|
||||
},
|
||||
"all_tags": [
|
||||
"industrial",
|
||||
"command",
|
||||
"measurement",
|
||||
"municipal",
|
||||
"fault",
|
||||
"emergency",
|
||||
"monitoring",
|
||||
"control",
|
||||
"primary",
|
||||
"water_system",
|
||||
"active",
|
||||
"controller",
|
||||
"sensor",
|
||||
"diagnostic",
|
||||
"status",
|
||||
"optimization",
|
||||
"setpoint",
|
||||
"automation",
|
||||
"maintenance",
|
||||
"backup",
|
||||
"remote",
|
||||
"pump",
|
||||
"secondary",
|
||||
"local",
|
||||
"alarm",
|
||||
"inactive",
|
||||
"feedback",
|
||||
"safety",
|
||||
"valve",
|
||||
"motor",
|
||||
"actuator",
|
||||
"healthy"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
"""
|
||||
Metadata Initializer
|
||||
|
||||
Loads sample metadata on application startup for demonstration purposes.
|
||||
In production, this would be replaced with actual metadata from a database or configuration.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .tag_metadata_manager import tag_metadata_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def initialize_sample_metadata():
|
||||
"""Initialize the system with sample metadata for demonstration"""
|
||||
|
||||
# Check if metadata file exists
|
||||
metadata_file = os.path.join(os.path.dirname(__file__), '..', '..', 'sample_metadata.json')
|
||||
|
||||
if os.path.exists(metadata_file):
|
||||
try:
|
||||
with open(metadata_file, 'r') as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
# Import metadata
|
||||
tag_metadata_manager.import_metadata(metadata)
|
||||
logger.info(f"Sample metadata loaded from {metadata_file}")
|
||||
logger.info(f"Loaded: {len(tag_metadata_manager.stations)} stations, "
|
||||
f"{len(tag_metadata_manager.equipment)} equipment, "
|
||||
f"{len(tag_metadata_manager.data_types)} data types")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load sample metadata: {str(e)}")
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"Sample metadata file not found: {metadata_file}")
|
||||
logger.info("System will start with empty metadata. Use the UI to create metadata.")
|
||||
return False
|
||||
|
||||
|
||||
def get_metadata_summary() -> dict:
|
||||
"""Get a summary of current metadata"""
|
||||
return {
|
||||
"stations": len(tag_metadata_manager.stations),
|
||||
"equipment": len(tag_metadata_manager.equipment),
|
||||
"data_types": len(tag_metadata_manager.data_types),
|
||||
"total_tags": len(tag_metadata_manager.all_tags)
|
||||
}
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
"""
|
||||
Metadata Manager for Calejo Control Adapter
|
||||
|
||||
Provides industry-agnostic metadata management for:
|
||||
- Stations/Assets
|
||||
- Equipment/Devices
|
||||
- Data types and signal mappings
|
||||
- Signal preprocessing rules
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any, Union
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel, validator
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class IndustryType(str, Enum):
|
||||
"""Supported industry types"""
|
||||
WASTEWATER = "wastewater"
|
||||
WATER_TREATMENT = "water_treatment"
|
||||
MANUFACTURING = "manufacturing"
|
||||
ENERGY = "energy"
|
||||
HVAC = "hvac"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
class DataCategory(str, Enum):
|
||||
"""Data categories for different signal types"""
|
||||
CONTROL = "control" # Setpoints, commands
|
||||
MONITORING = "monitoring" # Status, measurements
|
||||
SAFETY = "safety" # Safety limits, emergency stops
|
||||
DIAGNOSTIC = "diagnostic" # Diagnostics, health
|
||||
OPTIMIZATION = "optimization" # Optimization outputs
|
||||
|
||||
|
||||
class SignalTransformation(BaseModel):
|
||||
"""Signal transformation rule for preprocessing"""
|
||||
name: str
|
||||
transformation_type: str # scale, offset, clamp, linear_map, custom
|
||||
parameters: Dict[str, Any]
|
||||
description: str = ""
|
||||
|
||||
@validator('transformation_type')
|
||||
def validate_transformation_type(cls, v):
|
||||
valid_types = ['scale', 'offset', 'clamp', 'linear_map', 'custom']
|
||||
if v not in valid_types:
|
||||
raise ValueError(f"Transformation type must be one of: {valid_types}")
|
||||
return v
|
||||
|
||||
|
||||
class DataTypeMapping(BaseModel):
|
||||
"""Data type mapping configuration"""
|
||||
data_type: str
|
||||
category: DataCategory
|
||||
unit: str
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
default_value: Optional[float] = None
|
||||
transformation_rules: List[SignalTransformation] = []
|
||||
description: str = ""
|
||||
|
||||
|
||||
class AssetMetadata(BaseModel):
|
||||
"""Base asset metadata (station/equipment)"""
|
||||
asset_id: str
|
||||
name: str
|
||||
industry_type: IndustryType
|
||||
location: Optional[str] = None
|
||||
coordinates: Optional[Dict[str, float]] = None
|
||||
metadata: Dict[str, Any] = {}
|
||||
|
||||
@validator('asset_id')
|
||||
def validate_asset_id(cls, v):
|
||||
if not v.replace('_', '').isalnum():
|
||||
raise ValueError("Asset ID must be alphanumeric with underscores")
|
||||
return v
|
||||
|
||||
|
||||
class StationMetadata(AssetMetadata):
|
||||
"""Station/Plant metadata"""
|
||||
station_type: str = "general"
|
||||
capacity: Optional[float] = None
|
||||
equipment_count: int = 0
|
||||
|
||||
|
||||
class EquipmentMetadata(AssetMetadata):
|
||||
"""Equipment/Device metadata"""
|
||||
station_id: str
|
||||
equipment_type: str
|
||||
manufacturer: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
control_type: Optional[str] = None
|
||||
rated_power: Optional[float] = None
|
||||
min_operating_value: Optional[float] = None
|
||||
max_operating_value: Optional[float] = None
|
||||
default_setpoint: Optional[float] = None
|
||||
|
||||
|
||||
class MetadataManager:
|
||||
"""Manages metadata across different industries and data sources"""
|
||||
|
||||
def __init__(self, db_client=None):
|
||||
self.db_client = db_client
|
||||
self.stations: Dict[str, StationMetadata] = {}
|
||||
self.equipment: Dict[str, EquipmentMetadata] = {}
|
||||
self.data_types: Dict[str, DataTypeMapping] = {}
|
||||
self.industry_configs: Dict[IndustryType, Dict[str, Any]] = {}
|
||||
|
||||
# Initialize with default data types
|
||||
self._initialize_default_data_types()
|
||||
|
||||
def _initialize_default_data_types(self):
|
||||
"""Initialize default data types for common industries"""
|
||||
|
||||
# Control data types
|
||||
self.data_types["setpoint"] = DataTypeMapping(
|
||||
data_type="setpoint",
|
||||
category=DataCategory.CONTROL,
|
||||
unit="Hz",
|
||||
min_value=20.0,
|
||||
max_value=50.0,
|
||||
default_value=35.0,
|
||||
description="Frequency setpoint for VFD control"
|
||||
)
|
||||
|
||||
self.data_types["pressure_setpoint"] = DataTypeMapping(
|
||||
data_type="pressure_setpoint",
|
||||
category=DataCategory.CONTROL,
|
||||
unit="bar",
|
||||
min_value=0.0,
|
||||
max_value=10.0,
|
||||
description="Pressure setpoint for pump control"
|
||||
)
|
||||
|
||||
# Monitoring data types
|
||||
self.data_types["actual_speed"] = DataTypeMapping(
|
||||
data_type="actual_speed",
|
||||
category=DataCategory.MONITORING,
|
||||
unit="Hz",
|
||||
description="Actual motor speed"
|
||||
)
|
||||
|
||||
self.data_types["power"] = DataTypeMapping(
|
||||
data_type="power",
|
||||
category=DataCategory.MONITORING,
|
||||
unit="kW",
|
||||
description="Power consumption"
|
||||
)
|
||||
|
||||
self.data_types["flow"] = DataTypeMapping(
|
||||
data_type="flow",
|
||||
category=DataCategory.MONITORING,
|
||||
unit="m³/h",
|
||||
description="Flow rate"
|
||||
)
|
||||
|
||||
self.data_types["level"] = DataTypeMapping(
|
||||
data_type="level",
|
||||
category=DataCategory.MONITORING,
|
||||
unit="m",
|
||||
description="Liquid level"
|
||||
)
|
||||
|
||||
# Safety data types
|
||||
self.data_types["emergency_stop"] = DataTypeMapping(
|
||||
data_type="emergency_stop",
|
||||
category=DataCategory.SAFETY,
|
||||
unit="boolean",
|
||||
description="Emergency stop status"
|
||||
)
|
||||
|
||||
# Optimization data types
|
||||
self.data_types["optimized_setpoint"] = DataTypeMapping(
|
||||
data_type="optimized_setpoint",
|
||||
category=DataCategory.OPTIMIZATION,
|
||||
unit="Hz",
|
||||
min_value=20.0,
|
||||
max_value=50.0,
|
||||
description="Optimized frequency setpoint from AI/ML"
|
||||
)
|
||||
|
||||
def add_station(self, station: StationMetadata) -> bool:
|
||||
"""Add a station to metadata manager"""
|
||||
try:
|
||||
self.stations[station.asset_id] = station
|
||||
logger.info("station_added", station_id=station.asset_id, industry=station.industry_type)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("failed_to_add_station", station_id=station.asset_id, error=str(e))
|
||||
return False
|
||||
|
||||
def add_equipment(self, equipment: EquipmentMetadata) -> bool:
|
||||
"""Add equipment to metadata manager"""
|
||||
try:
|
||||
# Verify station exists
|
||||
if equipment.station_id not in self.stations:
|
||||
logger.warning("unknown_station_for_equipment",
|
||||
equipment_id=equipment.asset_id, station_id=equipment.station_id)
|
||||
|
||||
self.equipment[equipment.asset_id] = equipment
|
||||
|
||||
# Update station equipment count
|
||||
if equipment.station_id in self.stations:
|
||||
self.stations[equipment.station_id].equipment_count += 1
|
||||
|
||||
logger.info("equipment_added",
|
||||
equipment_id=equipment.asset_id,
|
||||
station_id=equipment.station_id,
|
||||
equipment_type=equipment.equipment_type)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("failed_to_add_equipment", equipment_id=equipment.asset_id, error=str(e))
|
||||
return False
|
||||
|
||||
def add_data_type(self, data_type: DataTypeMapping) -> bool:
|
||||
"""Add a custom data type"""
|
||||
try:
|
||||
self.data_types[data_type.data_type] = data_type
|
||||
logger.info("data_type_added", data_type=data_type.data_type, category=data_type.category)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("failed_to_add_data_type", data_type=data_type.data_type, error=str(e))
|
||||
return False
|
||||
|
||||
def get_stations(self, industry_type: Optional[IndustryType] = None) -> List[StationMetadata]:
|
||||
"""Get all stations, optionally filtered by industry"""
|
||||
if industry_type:
|
||||
return [station for station in self.stations.values()
|
||||
if station.industry_type == industry_type]
|
||||
return list(self.stations.values())
|
||||
|
||||
def get_equipment(self, station_id: Optional[str] = None) -> List[EquipmentMetadata]:
|
||||
"""Get all equipment, optionally filtered by station"""
|
||||
if station_id:
|
||||
return [equip for equip in self.equipment.values()
|
||||
if equip.station_id == station_id]
|
||||
return list(self.equipment.values())
|
||||
|
||||
def get_data_types(self, category: Optional[DataCategory] = None) -> List[DataTypeMapping]:
|
||||
"""Get all data types, optionally filtered by category"""
|
||||
if category:
|
||||
return [dt for dt in self.data_types.values() if dt.category == category]
|
||||
return list(self.data_types.values())
|
||||
|
||||
def get_available_data_types_for_equipment(self, equipment_id: str) -> List[DataTypeMapping]:
|
||||
"""Get data types suitable for specific equipment"""
|
||||
equipment = self.equipment.get(equipment_id)
|
||||
if not equipment:
|
||||
return []
|
||||
|
||||
# Filter data types based on equipment type and industry
|
||||
suitable_types = []
|
||||
for data_type in self.data_types.values():
|
||||
# Basic filtering logic - can be extended based on equipment metadata
|
||||
if data_type.category in [DataCategory.CONTROL, DataCategory.MONITORING, DataCategory.OPTIMIZATION]:
|
||||
suitable_types.append(data_type)
|
||||
|
||||
return suitable_types
|
||||
|
||||
def apply_transformation(self, value: float, data_type: str) -> float:
|
||||
"""Apply transformation rules to a value"""
|
||||
if data_type not in self.data_types:
|
||||
return value
|
||||
|
||||
data_type_config = self.data_types[data_type]
|
||||
transformed_value = value
|
||||
|
||||
for transformation in data_type_config.transformation_rules:
|
||||
transformed_value = self._apply_single_transformation(transformed_value, transformation)
|
||||
|
||||
return transformed_value
|
||||
|
||||
def _apply_single_transformation(self, value: float, transformation: SignalTransformation) -> float:
|
||||
"""Apply a single transformation rule"""
|
||||
params = transformation.parameters
|
||||
|
||||
if transformation.transformation_type == "scale":
|
||||
return value * params.get("factor", 1.0)
|
||||
|
||||
elif transformation.transformation_type == "offset":
|
||||
return value + params.get("offset", 0.0)
|
||||
|
||||
elif transformation.transformation_type == "clamp":
|
||||
min_val = params.get("min", float('-inf'))
|
||||
max_val = params.get("max", float('inf'))
|
||||
return max(min_val, min(value, max_val))
|
||||
|
||||
elif transformation.transformation_type == "linear_map":
|
||||
# Map from [input_min, input_max] to [output_min, output_max]
|
||||
input_min = params.get("input_min", 0.0)
|
||||
input_max = params.get("input_max", 1.0)
|
||||
output_min = params.get("output_min", 0.0)
|
||||
output_max = params.get("output_max", 1.0)
|
||||
|
||||
if input_max == input_min:
|
||||
return output_min
|
||||
|
||||
normalized = (value - input_min) / (input_max - input_min)
|
||||
return output_min + normalized * (output_max - output_min)
|
||||
|
||||
# For custom transformations, would need to implement specific logic
|
||||
return value
|
||||
|
||||
def get_metadata_summary(self) -> Dict[str, Any]:
|
||||
"""Get summary of all metadata"""
|
||||
return {
|
||||
"station_count": len(self.stations),
|
||||
"equipment_count": len(self.equipment),
|
||||
"data_type_count": len(self.data_types),
|
||||
"stations_by_industry": {
|
||||
industry.value: len([s for s in self.stations.values() if s.industry_type == industry])
|
||||
for industry in IndustryType
|
||||
},
|
||||
"data_types_by_category": {
|
||||
category.value: len([dt for dt in self.data_types.values() if dt.category == category])
|
||||
for category in DataCategory
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Global metadata manager instance
|
||||
metadata_manager = MetadataManager()
|
||||
|
|
@ -236,6 +236,7 @@ class AuthorizationManager:
|
|||
"emergency_stop",
|
||||
"clear_emergency_stop",
|
||||
"view_alerts",
|
||||
"configure_safety_limits",
|
||||
"manage_pump_configuration",
|
||||
"view_system_metrics"
|
||||
},
|
||||
|
|
@ -246,6 +247,7 @@ class AuthorizationManager:
|
|||
"emergency_stop",
|
||||
"clear_emergency_stop",
|
||||
"view_alerts",
|
||||
"configure_safety_limits",
|
||||
"manage_pump_configuration",
|
||||
"view_system_metrics",
|
||||
"manage_users",
|
||||
|
|
|
|||
|
|
@ -1,308 +0,0 @@
|
|||
"""
|
||||
Tag-Based Metadata Manager
|
||||
|
||||
A flexible, tag-based metadata system that replaces the industry-specific approach.
|
||||
Users can define their own tags and attributes for stations, equipment, and data types.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any, Set
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, asdict
|
||||
import uuid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TagCategory(Enum):
|
||||
"""Core tag categories for consistency"""
|
||||
FUNCTION = "function"
|
||||
SIGNAL_TYPE = "signal_type"
|
||||
EQUIPMENT_TYPE = "equipment_type"
|
||||
LOCATION = "location"
|
||||
STATUS = "status"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tag:
|
||||
"""Individual tag with optional description"""
|
||||
name: str
|
||||
category: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetadataEntity:
|
||||
"""Base class for all metadata entities"""
|
||||
id: str
|
||||
name: str
|
||||
tags: List[str]
|
||||
attributes: Dict[str, Any]
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Station(MetadataEntity):
|
||||
"""Station metadata"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Equipment(MetadataEntity):
|
||||
"""Equipment metadata"""
|
||||
station_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataType(MetadataEntity):
|
||||
"""Data type metadata"""
|
||||
units: Optional[str] = None
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
default_value: Optional[float] = None
|
||||
|
||||
|
||||
class TagMetadataManager:
|
||||
"""
|
||||
Tag-based metadata management system
|
||||
|
||||
Features:
|
||||
- User-defined tags and attributes
|
||||
- System-suggested core tags
|
||||
- Flexible search and filtering
|
||||
- No industry-specific assumptions
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.stations: Dict[str, Station] = {}
|
||||
self.equipment: Dict[str, Equipment] = {}
|
||||
self.data_types: Dict[str, DataType] = {}
|
||||
self.all_tags: Set[str] = set()
|
||||
|
||||
# Core suggested tags (users can ignore these)
|
||||
self._initialize_core_tags()
|
||||
|
||||
logger.info("TagMetadataManager initialized with tag-based approach")
|
||||
|
||||
def _initialize_core_tags(self):
|
||||
"""Initialize core suggested tags for consistency"""
|
||||
core_tags = {
|
||||
# Function tags
|
||||
"control", "monitoring", "safety", "diagnostic", "optimization",
|
||||
|
||||
# Signal type tags
|
||||
"setpoint", "measurement", "status", "alarm", "command", "feedback",
|
||||
|
||||
# Equipment type tags
|
||||
"pump", "valve", "motor", "sensor", "controller", "actuator",
|
||||
|
||||
# Location tags
|
||||
"primary", "secondary", "backup", "emergency", "remote", "local",
|
||||
|
||||
# Status tags
|
||||
"active", "inactive", "maintenance", "fault", "healthy"
|
||||
}
|
||||
|
||||
self.all_tags.update(core_tags)
|
||||
|
||||
def add_station(self,
|
||||
name: str,
|
||||
tags: List[str] = None,
|
||||
attributes: Dict[str, Any] = None,
|
||||
description: str = None,
|
||||
station_id: str = None) -> str:
|
||||
"""Add a new station"""
|
||||
station_id = station_id or f"station_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
station = Station(
|
||||
id=station_id,
|
||||
name=name,
|
||||
tags=tags or [],
|
||||
attributes=attributes or {},
|
||||
description=description
|
||||
)
|
||||
|
||||
self.stations[station_id] = station
|
||||
self.all_tags.update(station.tags)
|
||||
|
||||
logger.info(f"Added station: {station_id} with tags: {station.tags}")
|
||||
return station_id
|
||||
|
||||
def add_equipment(self,
|
||||
name: str,
|
||||
station_id: str,
|
||||
tags: List[str] = None,
|
||||
attributes: Dict[str, Any] = None,
|
||||
description: str = None,
|
||||
equipment_id: str = None) -> str:
|
||||
"""Add new equipment to a station"""
|
||||
if station_id not in self.stations:
|
||||
raise ValueError(f"Station {station_id} does not exist")
|
||||
|
||||
equipment_id = equipment_id or f"equipment_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
equipment = Equipment(
|
||||
id=equipment_id,
|
||||
name=name,
|
||||
station_id=station_id,
|
||||
tags=tags or [],
|
||||
attributes=attributes or {},
|
||||
description=description
|
||||
)
|
||||
|
||||
self.equipment[equipment_id] = equipment
|
||||
self.all_tags.update(equipment.tags)
|
||||
|
||||
logger.info(f"Added equipment: {equipment_id} to station {station_id}")
|
||||
return equipment_id
|
||||
|
||||
def add_data_type(self,
|
||||
name: str,
|
||||
tags: List[str] = None,
|
||||
attributes: Dict[str, Any] = None,
|
||||
description: str = None,
|
||||
units: str = None,
|
||||
min_value: float = None,
|
||||
max_value: float = None,
|
||||
default_value: float = None,
|
||||
data_type_id: str = None) -> str:
|
||||
"""Add a new data type"""
|
||||
data_type_id = data_type_id or f"datatype_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
data_type = DataType(
|
||||
id=data_type_id,
|
||||
name=name,
|
||||
tags=tags or [],
|
||||
attributes=attributes or {},
|
||||
description=description,
|
||||
units=units,
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
default_value=default_value
|
||||
)
|
||||
|
||||
self.data_types[data_type_id] = data_type
|
||||
self.all_tags.update(data_type.tags)
|
||||
|
||||
logger.info(f"Added data type: {data_type_id} with tags: {data_type.tags}")
|
||||
return data_type_id
|
||||
|
||||
def get_stations_by_tags(self, tags: List[str]) -> List[Station]:
|
||||
"""Get stations that have ALL specified tags"""
|
||||
return [
|
||||
station for station in self.stations.values()
|
||||
if all(tag in station.tags for tag in tags)
|
||||
]
|
||||
|
||||
def get_equipment_by_tags(self, tags: List[str], station_id: str = None) -> List[Equipment]:
|
||||
"""Get equipment that has ALL specified tags"""
|
||||
equipment_list = self.equipment.values()
|
||||
|
||||
if station_id:
|
||||
equipment_list = [eq for eq in equipment_list if eq.station_id == station_id]
|
||||
|
||||
return [
|
||||
equipment for equipment in equipment_list
|
||||
if all(tag in equipment.tags for tag in tags)
|
||||
]
|
||||
|
||||
def get_data_types_by_tags(self, tags: List[str]) -> List[DataType]:
|
||||
"""Get data types that have ALL specified tags"""
|
||||
return [
|
||||
data_type for data_type in self.data_types.values()
|
||||
if all(tag in data_type.tags for tag in tags)
|
||||
]
|
||||
|
||||
def search_by_tags(self, tags: List[str]) -> Dict[str, List[Any]]:
|
||||
"""Search across all entities by tags"""
|
||||
return {
|
||||
"stations": self.get_stations_by_tags(tags),
|
||||
"equipment": self.get_equipment_by_tags(tags),
|
||||
"data_types": self.get_data_types_by_tags(tags)
|
||||
}
|
||||
|
||||
def get_suggested_tags(self) -> List[str]:
|
||||
"""Get all available tags (core + user-defined)"""
|
||||
return sorted(list(self.all_tags))
|
||||
|
||||
def get_metadata_summary(self) -> Dict[str, Any]:
|
||||
"""Get summary of all metadata"""
|
||||
return {
|
||||
"stations_count": len(self.stations),
|
||||
"equipment_count": len(self.equipment),
|
||||
"data_types_count": len(self.data_types),
|
||||
"total_tags": len(self.all_tags),
|
||||
"suggested_tags": self.get_suggested_tags(),
|
||||
"stations": [asdict(station) for station in self.stations.values()],
|
||||
"equipment": [asdict(eq) for eq in self.equipment.values()],
|
||||
"data_types": [asdict(dt) for dt in self.data_types.values()]
|
||||
}
|
||||
|
||||
def add_custom_tag(self, tag: str):
|
||||
"""Add a custom tag to the system"""
|
||||
if tag and tag.strip():
|
||||
self.all_tags.add(tag.strip().lower())
|
||||
logger.info(f"Added custom tag: {tag}")
|
||||
|
||||
def remove_tag_from_entity(self, entity_type: str, entity_id: str, tag: str):
|
||||
"""Remove a tag from a specific entity"""
|
||||
entity_map = {
|
||||
"station": self.stations,
|
||||
"equipment": self.equipment,
|
||||
"data_type": self.data_types
|
||||
}
|
||||
|
||||
if entity_type not in entity_map:
|
||||
raise ValueError(f"Invalid entity type: {entity_type}")
|
||||
|
||||
entity = entity_map[entity_type].get(entity_id)
|
||||
if not entity:
|
||||
raise ValueError(f"{entity_type} {entity_id} not found")
|
||||
|
||||
if tag in entity.tags:
|
||||
entity.tags.remove(tag)
|
||||
logger.info(f"Removed tag '{tag}' from {entity_type} {entity_id}")
|
||||
|
||||
def export_metadata(self) -> Dict[str, Any]:
|
||||
"""Export all metadata for backup/transfer"""
|
||||
return {
|
||||
"stations": {id: asdict(station) for id, station in self.stations.items()},
|
||||
"equipment": {id: asdict(eq) for id, eq in self.equipment.items()},
|
||||
"data_types": {id: asdict(dt) for id, dt in self.data_types.items()},
|
||||
"all_tags": list(self.all_tags)
|
||||
}
|
||||
|
||||
def import_metadata(self, data: Dict[str, Any]):
|
||||
"""Import metadata from backup"""
|
||||
try:
|
||||
# Clear existing data
|
||||
self.stations.clear()
|
||||
self.equipment.clear()
|
||||
self.data_types.clear()
|
||||
self.all_tags.clear()
|
||||
|
||||
# Import stations
|
||||
for station_id, station_data in data.get("stations", {}).items():
|
||||
self.stations[station_id] = Station(**station_data)
|
||||
|
||||
# Import equipment
|
||||
for eq_id, eq_data in data.get("equipment", {}).items():
|
||||
self.equipment[eq_id] = Equipment(**eq_data)
|
||||
|
||||
# Import data types
|
||||
for dt_id, dt_data in data.get("data_types", {}).items():
|
||||
self.data_types[dt_id] = DataType(**dt_data)
|
||||
|
||||
# Import tags
|
||||
self.all_tags.update(data.get("all_tags", []))
|
||||
|
||||
logger.info("Successfully imported metadata")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import metadata: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
# Global instance
|
||||
tag_metadata_manager = TagMetadataManager()
|
||||
|
|
@ -12,10 +12,10 @@ from pydantic import BaseModel, ValidationError
|
|||
|
||||
from config.settings import Settings
|
||||
from .configuration_manager import (
|
||||
configuration_manager, OPCUAConfig, ModbusTCPConfig, DataPointMapping, ProtocolType, ProtocolMapping
|
||||
configuration_manager, OPCUAConfig, ModbusTCPConfig, PumpStationConfig,
|
||||
PumpConfig, SafetyLimitsConfig, DataPointMapping, ProtocolType, ProtocolMapping
|
||||
)
|
||||
from src.discovery.protocol_discovery_persistent import persistent_discovery_service, DiscoveryStatus, DiscoveredEndpoint
|
||||
from src.core.tag_metadata_manager import tag_metadata_manager
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -218,7 +218,44 @@ async def configure_modbus_tcp_protocol(config: ModbusTCPConfig):
|
|||
logger.error(f"Error configuring Modbus TCP protocol: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to configure Modbus TCP protocol: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/configure/station")
|
||||
async def configure_pump_station(station: PumpStationConfig):
|
||||
"""Configure a pump station"""
|
||||
try:
|
||||
success = configuration_manager.add_pump_station(station)
|
||||
if success:
|
||||
return {"success": True, "message": f"Pump station {station.name} configured successfully"}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Failed to configure pump station")
|
||||
except Exception as e:
|
||||
logger.error(f"Error configuring pump station: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to configure pump station: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/configure/pump")
|
||||
async def configure_pump(pump: PumpConfig):
|
||||
"""Configure a pump"""
|
||||
try:
|
||||
success = configuration_manager.add_pump(pump)
|
||||
if success:
|
||||
return {"success": True, "message": f"Pump {pump.name} configured successfully"}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Failed to configure pump")
|
||||
except Exception as e:
|
||||
logger.error(f"Error configuring pump: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to configure pump: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/configure/safety-limits")
|
||||
async def configure_safety_limits(limits: SafetyLimitsConfig):
|
||||
"""Configure safety limits for a pump"""
|
||||
try:
|
||||
success = configuration_manager.set_safety_limits(limits)
|
||||
if success:
|
||||
return {"success": True, "message": f"Safety limits configured for pump {limits.pump_id}"}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Failed to configure safety limits")
|
||||
except Exception as e:
|
||||
logger.error(f"Error configuring safety limits: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to configure safety limits: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/configure/data-mapping")
|
||||
async def configure_data_mapping(mapping: DataPointMapping):
|
||||
|
|
@ -793,13 +830,13 @@ async def export_signals():
|
|||
async def get_protocol_mappings(
|
||||
protocol_type: Optional[str] = None,
|
||||
station_id: Optional[str] = None,
|
||||
equipment_id: Optional[str] = None
|
||||
pump_id: Optional[str] = None
|
||||
):
|
||||
"""Get protocol mappings with optional filtering"""
|
||||
try:
|
||||
# Convert protocol_type string to enum if provided
|
||||
protocol_enum = None
|
||||
if protocol_type and protocol_type != "all":
|
||||
if protocol_type:
|
||||
try:
|
||||
protocol_enum = ProtocolType(protocol_type)
|
||||
except ValueError:
|
||||
|
|
@ -808,7 +845,7 @@ async def get_protocol_mappings(
|
|||
mappings = configuration_manager.get_protocol_mappings(
|
||||
protocol_type=protocol_enum,
|
||||
station_id=station_id,
|
||||
equipment_id=equipment_id
|
||||
pump_id=pump_id
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -836,19 +873,14 @@ async def create_protocol_mapping(mapping_data: dict):
|
|||
# Create ProtocolMapping object
|
||||
import uuid
|
||||
mapping = ProtocolMapping(
|
||||
id=mapping_data.get("id") or f"{mapping_data.get('protocol_type')}_{mapping_data.get('station_id', 'unknown')}_{mapping_data.get('equipment_id', 'unknown')}_{uuid.uuid4().hex[:8]}",
|
||||
id=mapping_data.get("id") or f"{mapping_data.get('protocol_type')}_{mapping_data.get('station_id', 'unknown')}_{mapping_data.get('pump_id', 'unknown')}_{uuid.uuid4().hex[:8]}",
|
||||
protocol_type=protocol_enum,
|
||||
station_id=mapping_data.get("station_id"),
|
||||
equipment_id=mapping_data.get("equipment_id"),
|
||||
data_type_id=mapping_data.get("data_type_id"),
|
||||
pump_id=mapping_data.get("pump_id"),
|
||||
data_type=mapping_data.get("data_type"),
|
||||
protocol_address=mapping_data.get("protocol_address"),
|
||||
db_source=mapping_data.get("db_source"),
|
||||
transformation_rules=mapping_data.get("transformation_rules", []),
|
||||
preprocessing_enabled=mapping_data.get("preprocessing_enabled", False),
|
||||
preprocessing_rules=mapping_data.get("preprocessing_rules", []),
|
||||
min_output_value=mapping_data.get("min_output_value"),
|
||||
max_output_value=mapping_data.get("max_output_value"),
|
||||
default_output_value=mapping_data.get("default_output_value"),
|
||||
modbus_config=mapping_data.get("modbus_config"),
|
||||
opcua_config=mapping_data.get("opcua_config")
|
||||
)
|
||||
|
|
@ -891,8 +923,8 @@ async def update_protocol_mapping(mapping_id: str, mapping_data: dict):
|
|||
id=mapping_id, # Use the ID from URL
|
||||
protocol_type=protocol_enum or ProtocolType(mapping_data.get("protocol_type")),
|
||||
station_id=mapping_data.get("station_id"),
|
||||
equipment_id=mapping_data.get("equipment_id"),
|
||||
data_type_id=mapping_data.get("data_type_id"),
|
||||
pump_id=mapping_data.get("pump_id"),
|
||||
data_type=mapping_data.get("data_type"),
|
||||
protocol_address=mapping_data.get("protocol_address"),
|
||||
db_source=mapping_data.get("db_source"),
|
||||
transformation_rules=mapping_data.get("transformation_rules", []),
|
||||
|
|
@ -939,404 +971,6 @@ async def delete_protocol_mapping(mapping_id: str):
|
|||
|
||||
# Protocol Discovery API Endpoints
|
||||
|
||||
# Simplified Protocol Signals API Endpoints
|
||||
@dashboard_router.get("/protocol-signals")
|
||||
async def get_protocol_signals(
|
||||
tags: Optional[str] = None,
|
||||
protocol_type: Optional[str] = None,
|
||||
signal_name_contains: Optional[str] = None,
|
||||
enabled: Optional[bool] = True
|
||||
):
|
||||
"""Get protocol signals with simplified name + tags approach"""
|
||||
try:
|
||||
from .simplified_models import ProtocolSignalFilter, ProtocolType
|
||||
from .simplified_configuration_manager import simplified_configuration_manager
|
||||
|
||||
# Parse tags from comma-separated string
|
||||
tag_list = tags.split(",") if tags else None
|
||||
|
||||
# Convert protocol_type string to enum if provided
|
||||
protocol_enum = None
|
||||
if protocol_type:
|
||||
try:
|
||||
protocol_enum = ProtocolType(protocol_type)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid protocol type: {protocol_type}")
|
||||
|
||||
# Create filter
|
||||
filters = ProtocolSignalFilter(
|
||||
tags=tag_list,
|
||||
protocol_type=protocol_enum,
|
||||
signal_name_contains=signal_name_contains,
|
||||
enabled=enabled
|
||||
)
|
||||
|
||||
signals = simplified_configuration_manager.get_protocol_signals(filters)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"signals": [signal.dict() for signal in signals],
|
||||
"count": len(signals)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting protocol signals: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get protocol signals: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/protocol-signals/{signal_id}")
|
||||
async def get_protocol_signal(signal_id: str):
|
||||
"""Get a specific protocol signal by ID"""
|
||||
try:
|
||||
from .simplified_configuration_manager import simplified_configuration_manager
|
||||
|
||||
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
|
||||
|
||||
if not signal:
|
||||
raise HTTPException(status_code=404, detail=f"Protocol signal {signal_id} not found")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"signal": signal.dict()
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting protocol signal {signal_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get protocol signal: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/protocol-signals")
|
||||
async def create_protocol_signal(signal_data: dict):
|
||||
"""Create a new protocol signal with simplified name + tags"""
|
||||
try:
|
||||
from .simplified_models import ProtocolSignalCreate, ProtocolType
|
||||
from .simplified_configuration_manager import simplified_configuration_manager
|
||||
|
||||
# Convert protocol_type string to enum
|
||||
if "protocol_type" not in signal_data:
|
||||
raise HTTPException(status_code=400, detail="protocol_type is required")
|
||||
|
||||
try:
|
||||
protocol_enum = ProtocolType(signal_data["protocol_type"])
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid protocol type: {signal_data['protocol_type']}")
|
||||
|
||||
# Create ProtocolSignalCreate object
|
||||
signal_create = ProtocolSignalCreate(
|
||||
signal_name=signal_data.get("signal_name"),
|
||||
tags=signal_data.get("tags", []),
|
||||
protocol_type=protocol_enum,
|
||||
protocol_address=signal_data.get("protocol_address"),
|
||||
db_source=signal_data.get("db_source"),
|
||||
preprocessing_enabled=signal_data.get("preprocessing_enabled", False),
|
||||
preprocessing_rules=signal_data.get("preprocessing_rules", []),
|
||||
min_output_value=signal_data.get("min_output_value"),
|
||||
max_output_value=signal_data.get("max_output_value"),
|
||||
default_output_value=signal_data.get("default_output_value"),
|
||||
modbus_config=signal_data.get("modbus_config"),
|
||||
opcua_config=signal_data.get("opcua_config")
|
||||
)
|
||||
|
||||
# Validate configuration
|
||||
validation = simplified_configuration_manager.validate_signal_configuration(signal_create)
|
||||
if not validation["valid"]:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Configuration validation failed",
|
||||
"errors": validation["errors"],
|
||||
"warnings": validation["warnings"]
|
||||
}
|
||||
|
||||
# Add the signal
|
||||
success = simplified_configuration_manager.add_protocol_signal(signal_create)
|
||||
|
||||
if success:
|
||||
# Get the created signal to return
|
||||
signal_id = signal_create.generate_signal_id()
|
||||
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Protocol signal created successfully",
|
||||
"signal": signal.dict() if signal else None,
|
||||
"warnings": validation["warnings"]
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Failed to create protocol signal")
|
||||
|
||||
except ValidationError as e:
|
||||
logger.error(f"Validation error creating protocol signal: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating protocol signal: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create protocol signal: {str(e)}")
|
||||
|
||||
@dashboard_router.put("/protocol-signals/{signal_id}")
|
||||
async def update_protocol_signal(signal_id: str, signal_data: dict):
|
||||
"""Update an existing protocol signal"""
|
||||
try:
|
||||
from .simplified_models import ProtocolSignalUpdate, ProtocolType
|
||||
from .simplified_configuration_manager import simplified_configuration_manager
|
||||
|
||||
# Convert protocol_type string to enum if provided
|
||||
protocol_enum = None
|
||||
if "protocol_type" in signal_data:
|
||||
try:
|
||||
protocol_enum = ProtocolType(signal_data["protocol_type"])
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid protocol type: {signal_data['protocol_type']}")
|
||||
|
||||
# Create ProtocolSignalUpdate object
|
||||
update_data = ProtocolSignalUpdate(
|
||||
signal_name=signal_data.get("signal_name"),
|
||||
tags=signal_data.get("tags"),
|
||||
protocol_type=protocol_enum,
|
||||
protocol_address=signal_data.get("protocol_address"),
|
||||
db_source=signal_data.get("db_source"),
|
||||
preprocessing_enabled=signal_data.get("preprocessing_enabled"),
|
||||
preprocessing_rules=signal_data.get("preprocessing_rules"),
|
||||
min_output_value=signal_data.get("min_output_value"),
|
||||
max_output_value=signal_data.get("max_output_value"),
|
||||
default_output_value=signal_data.get("default_output_value"),
|
||||
modbus_config=signal_data.get("modbus_config"),
|
||||
opcua_config=signal_data.get("opcua_config"),
|
||||
enabled=signal_data.get("enabled")
|
||||
)
|
||||
|
||||
success = simplified_configuration_manager.update_protocol_signal(signal_id, update_data)
|
||||
|
||||
if success:
|
||||
# Get the updated signal to return
|
||||
signal = simplified_configuration_manager.get_protocol_signal(signal_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Protocol signal updated successfully",
|
||||
"signal": signal.dict() if signal else None
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"Protocol signal {signal_id} not found")
|
||||
|
||||
except ValidationError as e:
|
||||
logger.error(f"Validation error updating protocol signal: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating protocol signal {signal_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update protocol signal: {str(e)}")
|
||||
|
||||
@dashboard_router.delete("/protocol-signals/{signal_id}")
|
||||
async def delete_protocol_signal(signal_id: str):
|
||||
"""Delete a protocol signal"""
|
||||
try:
|
||||
from .simplified_configuration_manager import simplified_configuration_manager
|
||||
|
||||
success = simplified_configuration_manager.delete_protocol_signal(signal_id)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Protocol signal {signal_id} deleted successfully"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=f"Protocol signal {signal_id} not found")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting protocol signal {signal_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete protocol signal: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/protocol-signals/tags/all")
|
||||
async def get_all_signal_tags():
|
||||
"""Get all unique tags used across protocol signals"""
|
||||
try:
|
||||
from .simplified_configuration_manager import simplified_configuration_manager
|
||||
|
||||
all_tags = simplified_configuration_manager.get_all_tags()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"tags": all_tags,
|
||||
"count": len(all_tags)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all signal tags: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get signal tags: {str(e)}")
|
||||
|
||||
# Tag-Based Metadata API Endpoints
|
||||
|
||||
@dashboard_router.get("/metadata/summary")
|
||||
async def get_metadata_summary():
|
||||
"""Get tag-based metadata summary"""
|
||||
try:
|
||||
summary = tag_metadata_manager.get_metadata_summary()
|
||||
return {
|
||||
"success": True,
|
||||
"summary": summary
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting metadata summary: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get metadata summary: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/stations")
|
||||
async def get_stations(tags: Optional[str] = None):
|
||||
"""Get stations, optionally filtered by tags (comma-separated)"""
|
||||
try:
|
||||
tag_list = tags.split(",") if tags else []
|
||||
stations = tag_metadata_manager.get_stations_by_tags(tag_list)
|
||||
return {
|
||||
"success": True,
|
||||
"stations": stations,
|
||||
"count": len(stations)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting stations: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get stations: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/equipment")
|
||||
async def get_equipment(station_id: Optional[str] = None, tags: Optional[str] = None):
|
||||
"""Get equipment, optionally filtered by station and tags"""
|
||||
try:
|
||||
tag_list = tags.split(",") if tags else []
|
||||
equipment = tag_metadata_manager.get_equipment_by_tags(tag_list, station_id)
|
||||
return {
|
||||
"success": True,
|
||||
"equipment": equipment,
|
||||
"count": len(equipment)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting equipment: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get equipment: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/data-types")
|
||||
async def get_data_types(tags: Optional[str] = None):
|
||||
"""Get data types, optionally filtered by tags"""
|
||||
try:
|
||||
tag_list = tags.split(",") if tags else []
|
||||
data_types = tag_metadata_manager.get_data_types_by_tags(tag_list)
|
||||
return {
|
||||
"success": True,
|
||||
"data_types": data_types,
|
||||
"count": len(data_types)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting data types: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get data types: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/tags")
|
||||
async def get_suggested_tags():
|
||||
"""Get all available tags (core + user-defined)"""
|
||||
try:
|
||||
tags = tag_metadata_manager.get_suggested_tags()
|
||||
return {
|
||||
"success": True,
|
||||
"tags": tags,
|
||||
"count": len(tags)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting tags: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get tags: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/metadata/stations")
|
||||
async def create_station(station_data: dict):
|
||||
"""Create a new station with tags"""
|
||||
try:
|
||||
station_id = tag_metadata_manager.add_station(
|
||||
name=station_data.get("name"),
|
||||
tags=station_data.get("tags", []),
|
||||
attributes=station_data.get("attributes", {}),
|
||||
description=station_data.get("description"),
|
||||
station_id=station_data.get("id")
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"station_id": station_id,
|
||||
"message": "Station created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating station: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create station: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/metadata/equipment")
|
||||
async def create_equipment(equipment_data: dict):
|
||||
"""Create new equipment with tags"""
|
||||
try:
|
||||
equipment_id = tag_metadata_manager.add_equipment(
|
||||
name=equipment_data.get("name"),
|
||||
station_id=equipment_data.get("station_id"),
|
||||
tags=equipment_data.get("tags", []),
|
||||
attributes=equipment_data.get("attributes", {}),
|
||||
description=equipment_data.get("description"),
|
||||
equipment_id=equipment_data.get("id")
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"equipment_id": equipment_id,
|
||||
"message": "Equipment created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating equipment: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create equipment: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/metadata/data-types")
|
||||
async def create_data_type(data_type_data: dict):
|
||||
"""Create new data type with tags"""
|
||||
try:
|
||||
data_type_id = tag_metadata_manager.add_data_type(
|
||||
name=data_type_data.get("name"),
|
||||
tags=data_type_data.get("tags", []),
|
||||
attributes=data_type_data.get("attributes", {}),
|
||||
description=data_type_data.get("description"),
|
||||
units=data_type_data.get("units"),
|
||||
min_value=data_type_data.get("min_value"),
|
||||
max_value=data_type_data.get("max_value"),
|
||||
default_value=data_type_data.get("default_value"),
|
||||
data_type_id=data_type_data.get("id")
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"data_type_id": data_type_id,
|
||||
"message": "Data type created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating data type: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create data type: {str(e)}")
|
||||
|
||||
@dashboard_router.post("/metadata/tags")
|
||||
async def add_custom_tag(tag_data: dict):
|
||||
"""Add a custom tag to the system"""
|
||||
try:
|
||||
tag = tag_data.get("tag")
|
||||
if not tag:
|
||||
raise HTTPException(status_code=400, detail="Tag is required")
|
||||
|
||||
tag_metadata_manager.add_custom_tag(tag)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Tag '{tag}' added successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding tag: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to add tag: {str(e)}")
|
||||
|
||||
@dashboard_router.get("/metadata/search")
|
||||
async def search_metadata(tags: str):
|
||||
"""Search across all metadata entities by tags"""
|
||||
try:
|
||||
if not tags:
|
||||
raise HTTPException(status_code=400, detail="Tags parameter is required")
|
||||
|
||||
tag_list = tags.split(",")
|
||||
results = tag_metadata_manager.search_by_tags(tag_list)
|
||||
return {
|
||||
"success": True,
|
||||
"search_tags": tag_list,
|
||||
"results": results
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching metadata: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to search metadata: {str(e)}")
|
||||
|
||||
|
||||
@dashboard_router.get("/discovery/status")
|
||||
async def get_discovery_status():
|
||||
"""Get current discovery service status"""
|
||||
|
|
@ -1463,7 +1097,7 @@ async def get_recent_discoveries():
|
|||
|
||||
|
||||
@dashboard_router.post("/discovery/apply/{scan_id}")
|
||||
async def apply_discovery_results(scan_id: str, station_id: str, equipment_id: str, data_type_id: str, db_source: str):
|
||||
async def apply_discovery_results(scan_id: str, station_id: str, pump_id: str, data_type: str, db_source: str):
|
||||
"""Apply discovered endpoints as protocol mappings"""
|
||||
try:
|
||||
result = persistent_discovery_service.get_scan_result(scan_id)
|
||||
|
|
@ -1480,29 +1114,15 @@ async def apply_discovery_results(scan_id: str, station_id: str, equipment_id: s
|
|||
for endpoint in result.get("discovered_endpoints", []):
|
||||
try:
|
||||
# Create protocol mapping from discovered endpoint
|
||||
mapping_id = f"{endpoint.get('device_id')}_{data_type_id}"
|
||||
|
||||
# Convert protocol types to match configuration manager expectations
|
||||
protocol_type = endpoint.get("protocol_type")
|
||||
if protocol_type == "opc_ua":
|
||||
protocol_type = "opcua"
|
||||
|
||||
# Convert addresses based on protocol type
|
||||
protocol_address = endpoint.get("address")
|
||||
if protocol_type == "modbus_tcp":
|
||||
# For Modbus TCP, use a default register address since IP is not valid
|
||||
protocol_address = "40001" # Default holding register
|
||||
elif protocol_type == "opcua":
|
||||
# For OPC UA, construct a proper node ID
|
||||
protocol_address = f"ns=2;s={endpoint.get('device_name', 'Device').replace(' ', '_')}"
|
||||
mapping_id = f"{endpoint.get('device_id')}_{data_type}"
|
||||
|
||||
protocol_mapping = ProtocolMapping(
|
||||
id=mapping_id,
|
||||
station_id=station_id,
|
||||
equipment_id=equipment_id,
|
||||
protocol_type=protocol_type,
|
||||
protocol_address=protocol_address,
|
||||
data_type_id=data_type_id,
|
||||
pump_id=pump_id,
|
||||
protocol_type=endpoint.get("protocol_type"),
|
||||
protocol_address=endpoint.get("address"),
|
||||
data_type=data_type,
|
||||
db_source=db_source
|
||||
)
|
||||
|
||||
|
|
@ -1547,8 +1167,8 @@ async def validate_protocol_mapping(mapping_id: str, mapping_data: dict):
|
|||
id=mapping_id,
|
||||
protocol_type=protocol_enum,
|
||||
station_id=mapping_data.get("station_id"),
|
||||
equipment_id=mapping_data.get("equipment_id"),
|
||||
data_type_id=mapping_data.get("data_type_id"),
|
||||
pump_id=mapping_data.get("pump_id"),
|
||||
data_type=mapping_data.get("data_type"),
|
||||
protocol_address=mapping_data.get("protocol_address"),
|
||||
db_source=mapping_data.get("db_source"),
|
||||
transformation_rules=mapping_data.get("transformation_rules", []),
|
||||
|
|
|
|||
|
|
@ -52,7 +52,57 @@ class ModbusTCPConfig(SCADAProtocolConfig):
|
|||
raise ValueError("Port must be between 1 and 65535")
|
||||
return v
|
||||
|
||||
class PumpStationConfig(BaseModel):
|
||||
"""Pump station configuration"""
|
||||
station_id: str
|
||||
name: str
|
||||
location: str = ""
|
||||
description: str = ""
|
||||
max_pumps: int = 4
|
||||
power_capacity: float = 150.0
|
||||
flow_capacity: float = 500.0
|
||||
|
||||
@validator('station_id')
|
||||
def validate_station_id(cls, v):
|
||||
if not v.replace('_', '').isalnum():
|
||||
raise ValueError("Station ID must be alphanumeric with underscores")
|
||||
return v
|
||||
|
||||
class PumpConfig(BaseModel):
|
||||
"""Individual pump configuration"""
|
||||
pump_id: str
|
||||
station_id: str
|
||||
name: str
|
||||
type: str = "centrifugal" # centrifugal, submersible, etc.
|
||||
power_rating: float # kW
|
||||
max_speed: float # Hz
|
||||
min_speed: float # Hz
|
||||
vfd_model: str = ""
|
||||
manufacturer: str = ""
|
||||
serial_number: str = ""
|
||||
|
||||
@validator('pump_id')
|
||||
def validate_pump_id(cls, v):
|
||||
if not v.replace('_', '').isalnum():
|
||||
raise ValueError("Pump ID must be alphanumeric with underscores")
|
||||
return v
|
||||
|
||||
class SafetyLimitsConfig(BaseModel):
|
||||
"""Safety limits configuration"""
|
||||
station_id: str
|
||||
pump_id: str
|
||||
hard_min_speed_hz: float = 20.0
|
||||
hard_max_speed_hz: float = 50.0
|
||||
hard_min_level_m: Optional[float] = None
|
||||
hard_max_level_m: Optional[float] = None
|
||||
hard_max_power_kw: Optional[float] = None
|
||||
max_speed_change_hz_per_min: float = 30.0
|
||||
|
||||
@validator('hard_max_speed_hz')
|
||||
def validate_speed_limits(cls, v, values):
|
||||
if 'hard_min_speed_hz' in values and v <= values['hard_min_speed_hz']:
|
||||
raise ValueError("Maximum speed must be greater than minimum speed")
|
||||
return v
|
||||
|
||||
class DataPointMapping(BaseModel):
|
||||
"""Data point mapping between protocol and internal representation"""
|
||||
|
|
@ -68,19 +118,12 @@ class ProtocolMapping(BaseModel):
|
|||
id: str
|
||||
protocol_type: ProtocolType
|
||||
station_id: str
|
||||
equipment_id: str
|
||||
data_type_id: str
|
||||
pump_id: str
|
||||
data_type: str # setpoint, status, power, flow, level, safety, etc.
|
||||
protocol_address: str # register address or OPC UA node
|
||||
db_source: str # database table and column
|
||||
transformation_rules: List[Dict[str, Any]] = []
|
||||
|
||||
# Signal preprocessing configuration
|
||||
preprocessing_enabled: bool = False
|
||||
preprocessing_rules: List[Dict[str, Any]] = []
|
||||
min_output_value: Optional[float] = None
|
||||
max_output_value: Optional[float] = None
|
||||
default_output_value: Optional[float] = None
|
||||
|
||||
# Protocol-specific configurations
|
||||
modbus_config: Optional[Dict[str, Any]] = None
|
||||
opcua_config: Optional[Dict[str, Any]] = None
|
||||
|
|
@ -91,36 +134,6 @@ class ProtocolMapping(BaseModel):
|
|||
raise ValueError("Mapping ID must be alphanumeric with underscores")
|
||||
return v
|
||||
|
||||
@validator('station_id')
|
||||
def validate_station_id(cls, v):
|
||||
"""Validate that station exists in tag metadata system"""
|
||||
from src.core.tag_metadata_manager import tag_metadata_manager
|
||||
if v and v not in tag_metadata_manager.stations:
|
||||
raise ValueError(f"Station '{v}' does not exist in tag metadata system")
|
||||
return v
|
||||
|
||||
@validator('equipment_id')
|
||||
def validate_equipment_id(cls, v, values):
|
||||
"""Validate that equipment exists in tag metadata system and belongs to station"""
|
||||
from src.core.tag_metadata_manager import tag_metadata_manager
|
||||
if v and v not in tag_metadata_manager.equipment:
|
||||
raise ValueError(f"Equipment '{v}' does not exist in tag metadata system")
|
||||
|
||||
# Validate equipment belongs to station
|
||||
if 'station_id' in values and values['station_id']:
|
||||
equipment = tag_metadata_manager.equipment.get(v)
|
||||
if equipment and equipment.station_id != values['station_id']:
|
||||
raise ValueError(f"Equipment '{v}' does not belong to station '{values['station_id']}'")
|
||||
return v
|
||||
|
||||
@validator('data_type_id')
|
||||
def validate_data_type_id(cls, v):
|
||||
"""Validate that data type exists in tag metadata system"""
|
||||
from src.core.tag_metadata_manager import tag_metadata_manager
|
||||
if v and v not in tag_metadata_manager.data_types:
|
||||
raise ValueError(f"Data type '{v}' does not exist in tag metadata system")
|
||||
return v
|
||||
|
||||
@validator('protocol_address')
|
||||
def validate_protocol_address(cls, v, values):
|
||||
if 'protocol_type' in values:
|
||||
|
|
@ -145,58 +158,12 @@ class ProtocolMapping(BaseModel):
|
|||
if not v.startswith(('http://', 'https://')):
|
||||
raise ValueError("REST API endpoint must start with 'http://' or 'https://'")
|
||||
return v
|
||||
|
||||
def apply_preprocessing(self, value: float) -> float:
|
||||
"""Apply preprocessing rules to a value"""
|
||||
if not self.preprocessing_enabled:
|
||||
return value
|
||||
|
||||
processed_value = value
|
||||
|
||||
for rule in self.preprocessing_rules:
|
||||
rule_type = rule.get('type')
|
||||
params = rule.get('parameters', {})
|
||||
|
||||
if rule_type == 'scale':
|
||||
processed_value *= params.get('factor', 1.0)
|
||||
elif rule_type == 'offset':
|
||||
processed_value += params.get('offset', 0.0)
|
||||
elif rule_type == 'clamp':
|
||||
min_val = params.get('min', float('-inf'))
|
||||
max_val = params.get('max', float('inf'))
|
||||
processed_value = max(min_val, min(processed_value, max_val))
|
||||
elif rule_type == 'linear_map':
|
||||
# Map from [input_min, input_max] to [output_min, output_max]
|
||||
input_min = params.get('input_min', 0.0)
|
||||
input_max = params.get('input_max', 1.0)
|
||||
output_min = params.get('output_min', 0.0)
|
||||
output_max = params.get('output_max', 1.0)
|
||||
|
||||
if input_max == input_min:
|
||||
processed_value = output_min
|
||||
else:
|
||||
normalized = (processed_value - input_min) / (input_max - input_min)
|
||||
processed_value = output_min + normalized * (output_max - output_min)
|
||||
elif rule_type == 'deadband':
|
||||
# Apply deadband to prevent oscillation
|
||||
center = params.get('center', 0.0)
|
||||
width = params.get('width', 0.0)
|
||||
if abs(processed_value - center) <= width:
|
||||
processed_value = center
|
||||
|
||||
# Apply final output limits
|
||||
if self.min_output_value is not None:
|
||||
processed_value = max(self.min_output_value, processed_value)
|
||||
if self.max_output_value is not None:
|
||||
processed_value = min(self.max_output_value, processed_value)
|
||||
|
||||
return processed_value
|
||||
|
||||
class HardwareDiscoveryResult(BaseModel):
|
||||
"""Result from hardware auto-discovery"""
|
||||
success: bool
|
||||
discovered_stations: List[Dict[str, Any]] = []
|
||||
discovered_pumps: List[Dict[str, Any]] = []
|
||||
discovered_stations: List[PumpStationConfig] = []
|
||||
discovered_pumps: List[PumpConfig] = []
|
||||
errors: List[str] = []
|
||||
warnings: List[str] = []
|
||||
|
||||
|
|
@ -205,6 +172,9 @@ class ConfigurationManager:
|
|||
|
||||
def __init__(self, db_client=None):
|
||||
self.protocol_configs: Dict[ProtocolType, SCADAProtocolConfig] = {}
|
||||
self.stations: Dict[str, PumpStationConfig] = {}
|
||||
self.pumps: Dict[str, PumpConfig] = {}
|
||||
self.safety_limits: Dict[str, SafetyLimitsConfig] = {}
|
||||
self.data_mappings: List[DataPointMapping] = []
|
||||
self.protocol_mappings: List[ProtocolMapping] = []
|
||||
self.db_client = db_client
|
||||
|
|
@ -217,11 +187,11 @@ class ConfigurationManager:
|
|||
"""Load protocol mappings from database"""
|
||||
try:
|
||||
query = """
|
||||
SELECT mapping_id, station_id, equipment_id, protocol_type,
|
||||
protocol_address, data_type_id, db_source, enabled
|
||||
SELECT mapping_id, station_id, pump_id, protocol_type,
|
||||
protocol_address, data_type, db_source, enabled
|
||||
FROM protocol_mappings
|
||||
WHERE enabled = true
|
||||
ORDER BY station_id, equipment_id, protocol_type
|
||||
ORDER BY station_id, pump_id, protocol_type
|
||||
"""
|
||||
|
||||
results = self.db_client.execute_query(query)
|
||||
|
|
@ -235,10 +205,10 @@ class ConfigurationManager:
|
|||
mapping = ProtocolMapping(
|
||||
id=row['mapping_id'],
|
||||
station_id=row['station_id'],
|
||||
equipment_id=row['equipment_id'],
|
||||
pump_id=row['pump_id'],
|
||||
protocol_type=protocol_type,
|
||||
protocol_address=row['protocol_address'],
|
||||
data_type_id=row['data_type_id'],
|
||||
data_type=row['data_type'],
|
||||
db_source=row['db_source']
|
||||
)
|
||||
self.protocol_mappings.append(mapping)
|
||||
|
|
@ -260,7 +230,44 @@ class ConfigurationManager:
|
|||
logger.error(f"Failed to configure protocol {config.protocol_type}: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def add_pump_station(self, station: PumpStationConfig) -> bool:
|
||||
"""Add a pump station configuration"""
|
||||
try:
|
||||
self.stations[station.station_id] = station
|
||||
logger.info(f"Added pump station: {station.name} ({station.station_id})")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add pump station {station.station_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
def add_pump(self, pump: PumpConfig) -> bool:
|
||||
"""Add a pump configuration"""
|
||||
try:
|
||||
# Verify station exists
|
||||
if pump.station_id not in self.stations:
|
||||
raise ValueError(f"Station {pump.station_id} does not exist")
|
||||
|
||||
self.pumps[pump.pump_id] = pump
|
||||
logger.info(f"Added pump: {pump.name} ({pump.pump_id}) to station {pump.station_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add pump {pump.pump_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
def set_safety_limits(self, limits: SafetyLimitsConfig) -> bool:
|
||||
"""Set safety limits for a pump"""
|
||||
try:
|
||||
# Verify pump exists
|
||||
if limits.pump_id not in self.pumps:
|
||||
raise ValueError(f"Pump {limits.pump_id} does not exist")
|
||||
|
||||
key = f"{limits.station_id}_{limits.pump_id}"
|
||||
self.safety_limits[key] = limits
|
||||
logger.info(f"Set safety limits for pump {limits.pump_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set safety limits for {limits.pump_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
def map_data_point(self, mapping: DataPointMapping) -> bool:
|
||||
"""Map a data point between protocol and internal representation"""
|
||||
|
|
@ -300,14 +307,14 @@ class ConfigurationManager:
|
|||
if self.db_client:
|
||||
query = """
|
||||
INSERT INTO protocol_mappings
|
||||
(mapping_id, station_id, equipment_id, protocol_type, protocol_address, data_type_id, db_source, created_by, enabled)
|
||||
VALUES (:mapping_id, :station_id, :equipment_id, :protocol_type, :protocol_address, :data_type_id, :db_source, :created_by, :enabled)
|
||||
(mapping_id, station_id, pump_id, protocol_type, protocol_address, data_type, db_source, created_by, enabled)
|
||||
VALUES (:mapping_id, :station_id, :pump_id, :protocol_type, :protocol_address, :data_type, :db_source, :created_by, :enabled)
|
||||
ON CONFLICT (mapping_id) DO UPDATE SET
|
||||
station_id = EXCLUDED.station_id,
|
||||
equipment_id = EXCLUDED.equipment_id,
|
||||
pump_id = EXCLUDED.pump_id,
|
||||
protocol_type = EXCLUDED.protocol_type,
|
||||
protocol_address = EXCLUDED.protocol_address,
|
||||
data_type_id = EXCLUDED.data_type_id,
|
||||
data_type = EXCLUDED.data_type,
|
||||
db_source = EXCLUDED.db_source,
|
||||
enabled = EXCLUDED.enabled,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
|
|
@ -315,10 +322,10 @@ class ConfigurationManager:
|
|||
params = {
|
||||
'mapping_id': mapping.id,
|
||||
'station_id': mapping.station_id,
|
||||
'equipment_id': mapping.equipment_id,
|
||||
'pump_id': mapping.pump_id,
|
||||
'protocol_type': mapping.protocol_type.value,
|
||||
'protocol_address': mapping.protocol_address,
|
||||
'data_type_id': mapping.data_type_id,
|
||||
'data_type': mapping.data_type,
|
||||
'db_source': mapping.db_source,
|
||||
'created_by': 'dashboard',
|
||||
'enabled': True
|
||||
|
|
@ -326,7 +333,7 @@ class ConfigurationManager:
|
|||
self.db_client.execute(query, params)
|
||||
|
||||
self.protocol_mappings.append(mapping)
|
||||
logger.info(f"Added protocol mapping {mapping.id}: {mapping.protocol_type} for {mapping.station_id}/{mapping.equipment_id}")
|
||||
logger.info(f"Added protocol mapping {mapping.id}: {mapping.protocol_type} for {mapping.station_id}/{mapping.pump_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add protocol mapping {mapping.id}: {str(e)}")
|
||||
|
|
@ -335,8 +342,8 @@ class ConfigurationManager:
|
|||
def get_protocol_mappings(self,
|
||||
protocol_type: Optional[ProtocolType] = None,
|
||||
station_id: Optional[str] = None,
|
||||
equipment_id: Optional[str] = None) -> List[ProtocolMapping]:
|
||||
"""Get mappings filtered by protocol/station/equipment"""
|
||||
pump_id: Optional[str] = None) -> List[ProtocolMapping]:
|
||||
"""Get mappings filtered by protocol/station/pump"""
|
||||
filtered_mappings = self.protocol_mappings.copy()
|
||||
|
||||
if protocol_type:
|
||||
|
|
@ -345,8 +352,8 @@ class ConfigurationManager:
|
|||
if station_id:
|
||||
filtered_mappings = [m for m in filtered_mappings if m.station_id == station_id]
|
||||
|
||||
if equipment_id:
|
||||
filtered_mappings = [m for m in filtered_mappings if m.equipment_id == equipment_id]
|
||||
if pump_id:
|
||||
filtered_mappings = [m for m in filtered_mappings if m.pump_id == pump_id]
|
||||
|
||||
return filtered_mappings
|
||||
|
||||
|
|
@ -366,10 +373,10 @@ class ConfigurationManager:
|
|||
query = """
|
||||
UPDATE protocol_mappings
|
||||
SET station_id = :station_id,
|
||||
equipment_id = :equipment_id,
|
||||
pump_id = :pump_id,
|
||||
protocol_type = :protocol_type,
|
||||
protocol_address = :protocol_address,
|
||||
data_type_id = :data_type_id,
|
||||
data_type = :data_type,
|
||||
db_source = :db_source,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE mapping_id = :mapping_id
|
||||
|
|
@ -377,10 +384,10 @@ class ConfigurationManager:
|
|||
params = {
|
||||
'mapping_id': mapping_id,
|
||||
'station_id': updated_mapping.station_id,
|
||||
'equipment_id': updated_mapping.equipment_id,
|
||||
'pump_id': updated_mapping.pump_id,
|
||||
'protocol_type': updated_mapping.protocol_type.value,
|
||||
'protocol_address': updated_mapping.protocol_address,
|
||||
'data_type_id': updated_mapping.data_type_id,
|
||||
'data_type': updated_mapping.data_type,
|
||||
'db_source': updated_mapping.db_source
|
||||
}
|
||||
self.db_client.execute(query, params)
|
||||
|
|
@ -438,7 +445,7 @@ class ConfigurationManager:
|
|||
if (existing.id != mapping.id and
|
||||
existing.protocol_type == ProtocolType.MODBUS_TCP and
|
||||
existing.protocol_address == mapping.protocol_address):
|
||||
errors.append(f"Modbus address {mapping.protocol_address} already used by {existing.station_id}/{existing.equipment_id}")
|
||||
errors.append(f"Modbus address {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
|
||||
break
|
||||
|
||||
except ValueError:
|
||||
|
|
@ -454,7 +461,7 @@ class ConfigurationManager:
|
|||
if (existing.id != mapping.id and
|
||||
existing.protocol_type == ProtocolType.OPC_UA and
|
||||
existing.protocol_address == mapping.protocol_address):
|
||||
errors.append(f"OPC UA node {mapping.protocol_address} already used by {existing.station_id}/{existing.equipment_id}")
|
||||
errors.append(f"OPC UA node {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
|
||||
break
|
||||
|
||||
elif mapping.protocol_type == ProtocolType.MODBUS_RTU:
|
||||
|
|
@ -469,7 +476,7 @@ class ConfigurationManager:
|
|||
if (existing.id != mapping.id and
|
||||
existing.protocol_type == ProtocolType.MODBUS_RTU and
|
||||
existing.protocol_address == mapping.protocol_address):
|
||||
errors.append(f"Modbus RTU address {mapping.protocol_address} already used by {existing.station_id}/{existing.equipment_id}")
|
||||
errors.append(f"Modbus RTU address {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
|
||||
break
|
||||
|
||||
except ValueError:
|
||||
|
|
@ -485,7 +492,7 @@ class ConfigurationManager:
|
|||
if (existing.id != mapping.id and
|
||||
existing.protocol_type == ProtocolType.REST_API and
|
||||
existing.protocol_address == mapping.protocol_address):
|
||||
errors.append(f"REST API endpoint {mapping.protocol_address} already used by {existing.station_id}/{existing.equipment_id}")
|
||||
errors.append(f"REST API endpoint {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
|
||||
break
|
||||
|
||||
# Check database source format
|
||||
|
|
@ -510,25 +517,25 @@ class ConfigurationManager:
|
|||
if ProtocolType.OPC_UA in self.protocol_configs:
|
||||
logger.info("Performing OPC UA hardware discovery...")
|
||||
# Simulate discovering a station via OPC UA
|
||||
mock_station = {
|
||||
"station_id": "discovered_station_001",
|
||||
"name": "Discovered Pump Station",
|
||||
"location": "Building A",
|
||||
"max_pumps": 2,
|
||||
"power_capacity": 100.0
|
||||
}
|
||||
mock_station = PumpStationConfig(
|
||||
station_id="discovered_station_001",
|
||||
name="Discovered Pump Station",
|
||||
location="Building A",
|
||||
max_pumps=2,
|
||||
power_capacity=100.0
|
||||
)
|
||||
result.discovered_stations.append(mock_station)
|
||||
|
||||
# Simulate discovering pumps
|
||||
mock_pump = {
|
||||
"pump_id": "discovered_pump_001",
|
||||
"station_id": "discovered_station_001",
|
||||
"name": "Discovered Primary Pump",
|
||||
"type": "centrifugal",
|
||||
"power_rating": 55.0,
|
||||
"max_speed": 50.0,
|
||||
"min_speed": 20.0
|
||||
}
|
||||
mock_pump = PumpConfig(
|
||||
pump_id="discovered_pump_001",
|
||||
station_id="discovered_station_001",
|
||||
name="Discovered Primary Pump",
|
||||
type="centrifugal",
|
||||
power_rating=55.0,
|
||||
max_speed=50.0,
|
||||
min_speed=20.0
|
||||
)
|
||||
result.discovered_pumps.append(mock_pump)
|
||||
|
||||
# Mock Modbus discovery
|
||||
|
|
@ -585,6 +592,9 @@ class ConfigurationManager:
|
|||
# Create summary
|
||||
validation_result["summary"] = {
|
||||
"protocols_configured": len(self.protocol_configs),
|
||||
"stations_configured": len(self.stations),
|
||||
"pumps_configured": len(self.pumps),
|
||||
"safety_limits_set": len(self.safety_limits),
|
||||
"data_mappings": len(self.data_mappings),
|
||||
"protocol_mappings": len(self.protocol_mappings)
|
||||
}
|
||||
|
|
@ -595,6 +605,9 @@ class ConfigurationManager:
|
|||
"""Export complete configuration for backup"""
|
||||
return {
|
||||
"protocols": {pt.value: config.dict() for pt, config in self.protocol_configs.items()},
|
||||
"stations": {sid: station.dict() for sid, station in self.stations.items()},
|
||||
"pumps": {pid: pump.dict() for pid, pump in self.pumps.items()},
|
||||
"safety_limits": {key: limits.dict() for key, limits in self.safety_limits.items()},
|
||||
"data_mappings": [mapping.dict() for mapping in self.data_mappings],
|
||||
"protocol_mappings": [mapping.dict() for mapping in self.protocol_mappings]
|
||||
}
|
||||
|
|
@ -604,6 +617,9 @@ class ConfigurationManager:
|
|||
try:
|
||||
# Clear existing configuration
|
||||
self.protocol_configs.clear()
|
||||
self.stations.clear()
|
||||
self.pumps.clear()
|
||||
self.safety_limits.clear()
|
||||
self.data_mappings.clear()
|
||||
self.protocol_mappings.clear()
|
||||
|
||||
|
|
@ -618,6 +634,21 @@ class ConfigurationManager:
|
|||
config = SCADAProtocolConfig(**config_dict)
|
||||
self.protocol_configs[protocol_type] = config
|
||||
|
||||
# Import stations
|
||||
for sid, station_dict in config_data.get("stations", {}).items():
|
||||
station = PumpStationConfig(**station_dict)
|
||||
self.stations[sid] = station
|
||||
|
||||
# Import pumps
|
||||
for pid, pump_dict in config_data.get("pumps", {}).items():
|
||||
pump = PumpConfig(**pump_dict)
|
||||
self.pumps[pid] = pump
|
||||
|
||||
# Import safety limits
|
||||
for key, limits_dict in config_data.get("safety_limits", {}).items():
|
||||
limits = SafetyLimitsConfig(**limits_dict)
|
||||
self.safety_limits[key] = limits
|
||||
|
||||
# Import data mappings
|
||||
for mapping_dict in config_data.get("data_mappings", []):
|
||||
mapping = DataPointMapping(**mapping_dict)
|
||||
|
|
|
|||
|
|
@ -1,277 +0,0 @@
|
|||
"""
|
||||
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()
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
"""
|
||||
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"]
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
"""
|
||||
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>
|
||||
"""
|
||||
|
|
@ -563,9 +563,9 @@ DASHBOARD_HTML = """
|
|||
<tr style="background: #f8f9fa;">
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">ID</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Protocol</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Station (Name & ID)</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Equipment (Name & ID)</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Data Type (Name & ID)</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Station</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Pump</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Data Type</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Protocol Address</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Database Source</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; text-align: left;">Actions</th>
|
||||
|
|
@ -599,25 +599,25 @@ DASHBOARD_HTML = """
|
|||
</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>
|
||||
</select>
|
||||
<small style="color: #666;">Stations will be loaded from tag metadata system</small>
|
||||
<label for="station_id">Station ID:</label>
|
||||
<input type="text" id="station_id" name="station_id" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="equipment_id">Equipment:</label>
|
||||
<select id="equipment_id" name="equipment_id" required>
|
||||
<option value="">Select Equipment</option>
|
||||
</select>
|
||||
<small style="color: #666;">Equipment will be loaded based on selected station</small>
|
||||
<label for="pump_id">Pump ID:</label>
|
||||
<input type="text" id="pump_id" name="pump_id" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="data_type_id">Data Type:</label>
|
||||
<select id="data_type_id" name="data_type_id" required>
|
||||
<label for="data_type">Data Type:</label>
|
||||
<select id="data_type" name="data_type" required>
|
||||
<option value="">Select Data Type</option>
|
||||
<option value="setpoint">Setpoint</option>
|
||||
<option value="actual_speed">Actual Speed</option>
|
||||
<option value="status">Status</option>
|
||||
<option value="power">Power</option>
|
||||
<option value="flow">Flow</option>
|
||||
<option value="level">Level</option>
|
||||
<option value="safety">Safety</option>
|
||||
</select>
|
||||
<small style="color: #666;">Data types will be loaded from tag metadata system</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol_address">Protocol Address:</label>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ from src.core.optimization_manager import OptimizationPlanManager
|
|||
from src.core.setpoint_manager import SetpointManager
|
||||
from src.core.security import SecurityManager
|
||||
from src.core.compliance_audit import ComplianceAuditLogger
|
||||
from src.core.metadata_initializer import initialize_sample_metadata
|
||||
from src.monitoring.watchdog import DatabaseWatchdog
|
||||
from src.monitoring.alerts import AlertManager
|
||||
from src.monitoring.health_monitor import HealthMonitor
|
||||
|
|
@ -183,10 +182,6 @@ class CalejoControlAdapter:
|
|||
await persistent_discovery_service.initialize()
|
||||
logger.info("persistent_discovery_service_initialized")
|
||||
|
||||
# Initialize sample metadata for demonstration
|
||||
initialize_sample_metadata()
|
||||
logger.info("sample_metadata_initialized")
|
||||
|
||||
# Load safety limits
|
||||
await self.safety_enforcer.load_safety_limits()
|
||||
logger.info("safety_limits_loaded")
|
||||
|
|
|
|||
|
|
@ -1,18 +1,55 @@
|
|||
// Simplified Discovery Integration
|
||||
// Updated for simplified signal names + tags architecture
|
||||
/**
|
||||
* Protocol Discovery JavaScript
|
||||
* Handles auto-discovery of protocol endpoints and integration with protocol mapping
|
||||
*/
|
||||
|
||||
class SimplifiedProtocolDiscovery {
|
||||
class ProtocolDiscovery {
|
||||
constructor() {
|
||||
this.currentScanId = 'simplified-scan-123';
|
||||
this.currentScanId = null;
|
||||
this.scanInterval = null;
|
||||
this.isScanning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize discovery functionality
|
||||
*/
|
||||
init() {
|
||||
this.bindDiscoveryEvents();
|
||||
this.loadDiscoveryStatus();
|
||||
|
||||
// Auto-refresh discovery status every 5 seconds
|
||||
setInterval(() => {
|
||||
if (this.isScanning) {
|
||||
this.loadDiscoveryStatus();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind discovery event handlers
|
||||
*/
|
||||
bindDiscoveryEvents() {
|
||||
// Auto-fill signal form from discovery
|
||||
// Start discovery scan
|
||||
document.getElementById('start-discovery-scan')?.addEventListener('click', () => {
|
||||
this.startDiscoveryScan();
|
||||
});
|
||||
|
||||
// Stop discovery scan
|
||||
document.getElementById('stop-discovery-scan')?.addEventListener('click', () => {
|
||||
this.stopDiscoveryScan();
|
||||
});
|
||||
|
||||
// Apply discovery results
|
||||
document.getElementById('apply-discovery-results')?.addEventListener('click', () => {
|
||||
this.applyDiscoveryResults();
|
||||
});
|
||||
|
||||
// Refresh discovery status
|
||||
document.getElementById('refresh-discovery-status')?.addEventListener('click', () => {
|
||||
this.loadDiscoveryStatus();
|
||||
});
|
||||
|
||||
// Auto-fill protocol form from discovery
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('use-discovered-endpoint')) {
|
||||
this.useDiscoveredEndpoint(e.target.dataset.endpointId);
|
||||
|
|
@ -20,320 +57,342 @@ class SimplifiedProtocolDiscovery {
|
|||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Start a new discovery scan
|
||||
*/
|
||||
async startDiscoveryScan() {
|
||||
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;
|
||||
this.setScanningState(true);
|
||||
|
||||
const response = await fetch('/api/v1/dashboard/discovery/scan', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.currentScanId = result.scan_id;
|
||||
this.showNotification('Discovery scan started successfully', 'success');
|
||||
|
||||
// Start polling for scan completion
|
||||
this.pollScanStatus();
|
||||
} else {
|
||||
throw new Error(result.detail || 'Failed to start discovery scan');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Discovery scan failed:', error);
|
||||
this.showNotification('Discovery scan failed', 'error');
|
||||
this.isScanning = false;
|
||||
return [];
|
||||
console.error('Error starting discovery scan:', error);
|
||||
this.showNotification(`Failed to start discovery scan: ${error.message}`, 'error');
|
||||
this.setScanningState(false);
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
];
|
||||
/**
|
||||
* Stop current discovery scan
|
||||
*/
|
||||
async stopDiscoveryScan() {
|
||||
// Note: This would require additional API endpoint to stop scans
|
||||
// For now, we'll just stop polling
|
||||
if (this.scanInterval) {
|
||||
clearInterval(this.scanInterval);
|
||||
this.scanInterval = null;
|
||||
}
|
||||
this.setScanningState(false);
|
||||
this.showNotification('Discovery scan stopped', 'info');
|
||||
}
|
||||
|
||||
displayDiscoveryResults(suggestedSignals) {
|
||||
/**
|
||||
* Poll for scan completion
|
||||
*/
|
||||
async pollScanStatus() {
|
||||
if (!this.currentScanId) return;
|
||||
|
||||
this.scanInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/dashboard/discovery/results/${this.currentScanId}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
if (result.status === 'completed' || result.status === 'failed') {
|
||||
clearInterval(this.scanInterval);
|
||||
this.scanInterval = null;
|
||||
this.setScanningState(false);
|
||||
|
||||
if (result.status === 'completed') {
|
||||
this.showNotification(`Discovery scan completed. Found ${result.discovered_endpoints.length} endpoints`, 'success');
|
||||
this.displayDiscoveryResults(result);
|
||||
} else {
|
||||
this.showNotification('Discovery scan failed', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error polling scan status:', error);
|
||||
clearInterval(this.scanInterval);
|
||||
this.scanInterval = null;
|
||||
this.setScanningState(false);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current discovery status
|
||||
*/
|
||||
async loadDiscoveryStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/dashboard/discovery/status');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.updateDiscoveryStatusUI(result.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading discovery status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update discovery status UI
|
||||
*/
|
||||
updateDiscoveryStatusUI(status) {
|
||||
const statusElement = document.getElementById('discovery-status');
|
||||
const scanButton = document.getElementById('start-discovery-scan');
|
||||
const stopButton = document.getElementById('stop-discovery-scan');
|
||||
|
||||
if (!statusElement) return;
|
||||
|
||||
this.isScanning = status.is_scanning;
|
||||
|
||||
if (status.is_scanning) {
|
||||
statusElement.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-sync fa-spin"></i>
|
||||
Discovery scan in progress... (Scan ID: ${status.current_scan_id})
|
||||
</div>
|
||||
`;
|
||||
scanButton?.setAttribute('disabled', 'true');
|
||||
stopButton?.removeAttribute('disabled');
|
||||
} else {
|
||||
statusElement.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check"></i>
|
||||
Discovery service ready
|
||||
${status.total_discovered_endpoints > 0 ?
|
||||
`- ${status.total_discovered_endpoints} endpoints discovered` :
|
||||
''
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
scanButton?.removeAttribute('disabled');
|
||||
stopButton?.setAttribute('disabled', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display discovery results
|
||||
*/
|
||||
displayDiscoveryResults(result) {
|
||||
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('')}
|
||||
|
||||
const endpoints = result.discovered_endpoints || [];
|
||||
|
||||
if (endpoints.length === 0) {
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
No endpoints discovered in this scan
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-search"></i>
|
||||
Discovery Results (${endpoints.length} endpoints found)
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Protocol</th>
|
||||
<th>Device Name</th>
|
||||
<th>Address</th>
|
||||
<th>Capabilities</th>
|
||||
<th>Discovered</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
endpoints.forEach(endpoint => {
|
||||
const protocolBadge = this.getProtocolBadge(endpoint.protocol_type);
|
||||
const capabilities = endpoint.capabilities ? endpoint.capabilities.join(', ') : 'N/A';
|
||||
const discoveredTime = endpoint.discovered_at ?
|
||||
new Date(endpoint.discovered_at).toLocaleString() : 'N/A';
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>${protocolBadge}</td>
|
||||
<td>${endpoint.device_name || 'Unknown Device'}</td>
|
||||
<td><code>${endpoint.address}${endpoint.port ? ':' + endpoint.port : ''}</code></td>
|
||||
<td><small>${capabilities}</small></td>
|
||||
<td><small>${discoveredTime}</small></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary use-discovered-endpoint"
|
||||
data-endpoint-id="${endpoint.device_id}"
|
||||
title="Use this endpoint in protocol mapping">
|
||||
<i class="fas fa-plus"></i> Use
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="signal-details">
|
||||
<span>Protocol: ${signal.protocol_type}</span>
|
||||
<span>Address: ${signal.protocol_address}</span>
|
||||
<div class="mt-3">
|
||||
<button id="apply-discovery-results" class="btn btn-success">
|
||||
<i class="fas fa-check"></i>
|
||||
Apply All as Protocol Mappings
|
||||
</button>
|
||||
</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);
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
|
||||
resultsContainer.innerHTML = html;
|
||||
|
||||
// Re-bind apply button
|
||||
document.getElementById('apply-discovery-results')?.addEventListener('click', () => {
|
||||
this.applyDiscoveryResults();
|
||||
});
|
||||
}
|
||||
|
||||
// Tag-based signal search
|
||||
async searchSignalsByTags(tags) {
|
||||
/**
|
||||
* Apply discovery results as protocol mappings
|
||||
*/
|
||||
async applyDiscoveryResults() {
|
||||
if (!this.currentScanId) {
|
||||
this.showNotification('No discovery results to apply', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get station and pump info from form or prompt
|
||||
const stationId = document.getElementById('station-id')?.value || 'station_001';
|
||||
const pumpId = document.getElementById('pump-id')?.value || 'pump_001';
|
||||
const dataType = document.getElementById('data-type')?.value || 'setpoint';
|
||||
const dbSource = document.getElementById('db-source')?.value || 'frequency_hz';
|
||||
|
||||
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;
|
||||
const response = await fetch(`/api/v1/dashboard/discovery/apply/${this.currentScanId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
station_id: stationId,
|
||||
pump_id: pumpId,
|
||||
data_type: dataType,
|
||||
db_source: dbSource
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.showNotification(`Successfully created ${result.created_mappings.length} protocol mappings`, 'success');
|
||||
|
||||
// Refresh protocol mappings grid
|
||||
if (window.protocolMappingGrid) {
|
||||
window.protocolMappingGrid.loadProtocolMappings();
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to search signals by tags:', data.detail);
|
||||
return [];
|
||||
throw new Error(result.detail || 'Failed to apply discovery results');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error searching signals by tags:', error);
|
||||
return [];
|
||||
console.error('Error applying discovery results:', error);
|
||||
this.showNotification(`Failed to apply discovery results: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Signal name suggestions based on device type
|
||||
generateSignalNameSuggestions(deviceName, dataPoint) {
|
||||
const baseName = `${deviceName} ${dataPoint}`;
|
||||
/**
|
||||
* Use discovered endpoint in protocol form
|
||||
*/
|
||||
useDiscoveredEndpoint(endpointId) {
|
||||
// This would fetch the specific endpoint details and populate the form
|
||||
// For now, we'll just show a notification
|
||||
this.showNotification(`Endpoint ${endpointId} selected for protocol mapping`, 'info');
|
||||
|
||||
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;
|
||||
// In a real implementation, we would:
|
||||
// 1. Fetch endpoint details
|
||||
// 2. Populate protocol form fields
|
||||
// 3. Switch to protocol mapping tab
|
||||
}
|
||||
|
||||
// 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');
|
||||
/**
|
||||
* Set scanning state
|
||||
*/
|
||||
setScanningState(scanning) {
|
||||
this.isScanning = scanning;
|
||||
const scanButton = document.getElementById('start-discovery-scan');
|
||||
const stopButton = document.getElementById('stop-discovery-scan');
|
||||
|
||||
if (scanning) {
|
||||
scanButton?.setAttribute('disabled', 'true');
|
||||
stopButton?.removeAttribute('disabled');
|
||||
} else {
|
||||
scanButton?.removeAttribute('disabled');
|
||||
stopButton?.setAttribute('disabled', 'true');
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get protocol badge HTML
|
||||
*/
|
||||
getProtocolBadge(protocolType) {
|
||||
const badges = {
|
||||
'modbus_tcp': '<span class="badge bg-primary">Modbus TCP</span>',
|
||||
'modbus_rtu': '<span class="badge bg-info">Modbus RTU</span>',
|
||||
'opc_ua': '<span class="badge bg-success">OPC UA</span>',
|
||||
'rest_api': '<span class="badge bg-warning">REST API</span>'
|
||||
};
|
||||
|
||||
return badges[protocolType] || `<span class="badge bg-secondary">${protocolType}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification
|
||||
*/
|
||||
showNotification(message, type = 'info') {
|
||||
// Use existing notification system or create simple alert
|
||||
const alertClass = {
|
||||
'success': 'alert-success',
|
||||
'error': 'alert-danger',
|
||||
'warning': 'alert-warning',
|
||||
'info': 'alert-info'
|
||||
}[type] || 'alert-info';
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `discovery-notification ${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
notification.className = `alert ${alertClass} alert-dismissible fade show`;
|
||||
notification.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
const container = document.getElementById('discovery-notifications') || document.body;
|
||||
container.appendChild(notification);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
|
|
@ -343,10 +402,8 @@ class SimplifiedProtocolDiscovery {
|
|||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
const simplifiedDiscovery = new SimplifiedProtocolDiscovery();
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
simplifiedDiscovery.init();
|
||||
// Initialize discovery when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.protocolDiscovery = new ProtocolDiscovery();
|
||||
window.protocolDiscovery.init();
|
||||
});
|
||||
|
|
@ -1,157 +1,111 @@
|
|||
// Simplified Protocol Mapping Functions
|
||||
// Uses human-readable signal names and tags instead of complex IDs
|
||||
|
||||
// Protocol Mapping Functions
|
||||
let currentProtocolFilter = 'all';
|
||||
let editingSignalId = null;
|
||||
let allTags = new Set();
|
||||
let editingMappingId = null;
|
||||
|
||||
// Simplified Signal Management Functions
|
||||
async function loadAllSignals() {
|
||||
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 {
|
||||
const response = await fetch('/api/v1/dashboard/protocol-signals');
|
||||
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();
|
||||
|
||||
if (data.success) {
|
||||
displaySignals(data.signals);
|
||||
updateTagCloud(data.signals);
|
||||
displayProtocolMappings(data.mappings);
|
||||
} else {
|
||||
showSimplifiedAlert('Failed to load signals', 'error');
|
||||
showProtocolMappingAlert('Failed to load protocol mappings', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading signals:', error);
|
||||
showSimplifiedAlert('Error loading signals', 'error');
|
||||
console.error('Error loading protocol mappings:', error);
|
||||
showProtocolMappingAlert('Error loading protocol mappings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function displaySignals(signals) {
|
||||
const tbody = document.getElementById('protocol-signals-body');
|
||||
function displayProtocolMappings(mappings) {
|
||||
const tbody = document.getElementById('protocol-mappings-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>';
|
||||
if (mappings.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px;">No protocol mappings found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
signals.forEach(signal => {
|
||||
mappings.forEach(mapping => {
|
||||
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;">${mapping.id}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.protocol_type}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.station_id || '-'}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.pump_id || '-'}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.data_type}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.protocol_address}</td>
|
||||
<td style="padding: 10px; border: 1px solid #ddd;">${mapping.db_source}</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>
|
||||
<button onclick="editMapping('${mapping.id}')" style="background: #007acc; margin-right: 5px;">Edit</button>
|
||||
<button onclick="deleteMapping('${mapping.id}')" style="background: #dc3545;">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 showAddMappingModal() {
|
||||
editingMappingId = null;
|
||||
document.getElementById('modal-title').textContent = 'Add Protocol Mapping';
|
||||
document.getElementById('mapping-form').reset();
|
||||
document.getElementById('protocol_address_help').textContent = '';
|
||||
document.getElementById('mapping-modal').style.display = 'block';
|
||||
}
|
||||
|
||||
function 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;
|
||||
function showEditMappingModal(mapping) {
|
||||
editingMappingId = mapping.id;
|
||||
document.getElementById('modal-title').textContent = 'Edit Protocol Mapping';
|
||||
document.getElementById('mapping_id').value = mapping.id;
|
||||
document.getElementById('protocol_type').value = mapping.protocol_type;
|
||||
document.getElementById('station_id').value = mapping.station_id || '';
|
||||
document.getElementById('pump_id').value = mapping.pump_id || '';
|
||||
document.getElementById('data_type').value = mapping.data_type;
|
||||
document.getElementById('protocol_address').value = mapping.protocol_address;
|
||||
document.getElementById('db_source').value = mapping.db_source;
|
||||
|
||||
updateProtocolFields();
|
||||
document.getElementById('signal-modal').style.display = 'block';
|
||||
document.getElementById('mapping-modal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeSignalModal() {
|
||||
document.getElementById('signal-modal').style.display = 'none';
|
||||
editingSignalId = null;
|
||||
function closeMappingModal() {
|
||||
document.getElementById('mapping-modal').style.display = 'none';
|
||||
editingMappingId = null;
|
||||
}
|
||||
|
||||
function updateProtocolFields() {
|
||||
const protocolType = document.getElementById('protocol_type').value;
|
||||
const helpText = document.getElementById('protocol-address-help');
|
||||
const helpText = document.getElementById('protocol_address_help');
|
||||
|
||||
switch (protocolType) {
|
||||
case 'modbus_tcp':
|
||||
case 'modbus_rtu':
|
||||
helpText.textContent = 'Modbus address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
|
||||
break;
|
||||
case 'opcua':
|
||||
helpText.textContent = 'OPC UA NodeId format: ns=2;s=MyVariable or ns=2;i=1234';
|
||||
break;
|
||||
case 'modbus_rtu':
|
||||
helpText.textContent = 'Modbus RTU address format: 40001 (holding register), 30001 (input register), 10001 (coil), 00001 (discrete input)';
|
||||
break;
|
||||
case 'rest_api':
|
||||
helpText.textContent = 'REST API endpoint format: /api/v1/data/endpoint';
|
||||
break;
|
||||
|
|
@ -160,22 +114,48 @@ function updateProtocolFields() {
|
|||
}
|
||||
}
|
||||
|
||||
// Form Submission
|
||||
async function saveSignal(event) {
|
||||
async function validateMapping() {
|
||||
const formData = getMappingFormData();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/dashboard/protocol-mappings/${editingMappingId || 'new'}/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
if (data.valid) {
|
||||
showProtocolMappingAlert('Mapping validation successful!', 'success');
|
||||
} else {
|
||||
showProtocolMappingAlert(`Validation failed: ${data.errors.join(', ')}`, 'error');
|
||||
}
|
||||
} else {
|
||||
showProtocolMappingAlert('Validation error', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error validating mapping:', error);
|
||||
showProtocolMappingAlert('Error validating mapping', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMapping(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = getSignalFormData();
|
||||
const formData = getMappingFormData();
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (editingSignalId) {
|
||||
response = await fetch(`/api/v1/dashboard/protocol-signals/${editingSignalId}`, {
|
||||
if (editingMappingId) {
|
||||
response = await fetch(`/api/v1/dashboard/protocol-mappings/${editingMappingId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
} else {
|
||||
response = await fetch('/api/v1/dashboard/protocol-signals', {
|
||||
response = await fetch('/api/v1/dashboard/protocol-mappings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
|
|
@ -185,151 +165,76 @@ async function saveSignal(event) {
|
|||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showSimplifiedAlert(`Protocol signal ${editingSignalId ? 'updated' : 'created'} successfully!`, 'success');
|
||||
closeSignalModal();
|
||||
loadAllSignals();
|
||||
showProtocolMappingAlert(`Protocol mapping ${editingMappingId ? 'updated' : 'created'} successfully!`, 'success');
|
||||
closeMappingModal();
|
||||
loadProtocolMappings();
|
||||
} else {
|
||||
showSimplifiedAlert(`Failed to save signal: ${data.detail || 'Unknown error'}`, 'error');
|
||||
showProtocolMappingAlert(`Failed to save mapping: ${data.detail || 'Unknown error'}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving signal:', error);
|
||||
showSimplifiedAlert('Error saving signal', 'error');
|
||||
console.error('Error saving mapping:', error);
|
||||
showProtocolMappingAlert('Error saving mapping', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function getSignalFormData() {
|
||||
const tagsInput = document.getElementById('tags').value;
|
||||
const tags = tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||
|
||||
function getMappingFormData() {
|
||||
return {
|
||||
signal_name: document.getElementById('signal_name').value,
|
||||
tags: tags,
|
||||
protocol_type: document.getElementById('protocol_type').value,
|
||||
station_id: document.getElementById('station_id').value,
|
||||
pump_id: document.getElementById('pump_id').value,
|
||||
data_type: document.getElementById('data_type').value,
|
||||
protocol_address: document.getElementById('protocol_address').value,
|
||||
db_source: document.getElementById('db_source').value,
|
||||
preprocessing_enabled: document.getElementById('preprocessing_enabled').checked
|
||||
db_source: document.getElementById('db_source').value
|
||||
};
|
||||
}
|
||||
|
||||
// Signal Management
|
||||
async function editSignal(signalId) {
|
||||
async function editMapping(mappingId) {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/dashboard/protocol-signals/${signalId}`);
|
||||
const response = await fetch(`/api/v1/dashboard/protocol-mappings?protocol_type=all`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showEditSignalModal(data.signal);
|
||||
const mapping = data.mappings.find(m => m.id === mappingId);
|
||||
if (mapping) {
|
||||
showEditMappingModal(mapping);
|
||||
} else {
|
||||
showProtocolMappingAlert('Mapping not found', 'error');
|
||||
}
|
||||
} else {
|
||||
showSimplifiedAlert('Signal not found', 'error');
|
||||
showProtocolMappingAlert('Failed to load mapping', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading signal:', error);
|
||||
showSimplifiedAlert('Error loading signal', 'error');
|
||||
console.error('Error loading mapping:', error);
|
||||
showProtocolMappingAlert('Error loading mapping', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSignal(signalId) {
|
||||
if (!confirm('Are you sure you want to delete this signal?')) {
|
||||
async function deleteMapping(mappingId) {
|
||||
if (!confirm(`Are you sure you want to delete mapping ${mappingId}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/dashboard/protocol-signals/${signalId}`, {
|
||||
const response = await fetch(`/api/v1/dashboard/protocol-mappings/${mappingId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showSimplifiedAlert('Signal deleted successfully!', 'success');
|
||||
loadAllSignals();
|
||||
showProtocolMappingAlert('Mapping deleted successfully!', 'success');
|
||||
loadProtocolMappings();
|
||||
} else {
|
||||
showSimplifiedAlert(`Failed to delete signal: ${data.detail || 'Unknown error'}`, 'error');
|
||||
showProtocolMappingAlert(`Failed to delete mapping: ${data.detail || 'Unknown error'}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting signal:', error);
|
||||
showSimplifiedAlert('Error deleting signal', 'error');
|
||||
console.error('Error deleting mapping:', error);
|
||||
showProtocolMappingAlert('Error deleting mapping', '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');
|
||||
function showProtocolMappingAlert(message, type) {
|
||||
const alertsDiv = document.getElementById('protocol-mapping-alerts');
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert ${type === 'error' ? 'error' : 'success'}`;
|
||||
alertDiv.textContent = message;
|
||||
|
|
@ -337,21 +242,57 @@ function showSimplifiedAlert(message, type = 'info') {
|
|||
alertsDiv.innerHTML = '';
|
||||
alertsDiv.appendChild(alertDiv);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const signalForm = document.getElementById('signal-form');
|
||||
if (signalForm) {
|
||||
signalForm.addEventListener('submit', saveSignal);
|
||||
async function exportProtocolMappings() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/dashboard/protocol-mappings?protocol_type=all');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const csvContent = convertToCSV(data.mappings);
|
||||
downloadCSV(csvContent, 'protocol_mappings.csv');
|
||||
} else {
|
||||
showProtocolMappingAlert('Failed to export mappings', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error exporting mappings:', error);
|
||||
showProtocolMappingAlert('Error exporting mappings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function convertToCSV(mappings) {
|
||||
const headers = ['ID', 'Protocol', 'Station', 'Pump', 'Data Type', 'Protocol Address', 'Database Source'];
|
||||
const rows = mappings.map(mapping => [
|
||||
mapping.id,
|
||||
mapping.protocol_type,
|
||||
mapping.station_id || '',
|
||||
mapping.pump_id || '',
|
||||
mapping.data_type,
|
||||
mapping.protocol_address,
|
||||
mapping.db_source
|
||||
]);
|
||||
|
||||
// Load initial data
|
||||
loadAllSignals();
|
||||
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() {
|
||||
const mappingForm = document.getElementById('mapping-form');
|
||||
if (mappingForm) {
|
||||
mappingForm.addEventListener('submit', saveMapping);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
// 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();
|
||||
});
|
||||
|
|
@ -1,357 +0,0 @@
|
|||
// 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();
|
||||
});
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
* {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
#!/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())
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
// 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);
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
#!/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()
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
#!/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())
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Use Button Functionality</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.endpoint { border: 1px solid #ccc; padding: 10px; margin: 10px 0; }
|
||||
.use-btn { background: #007bff; color: white; border: none; padding: 5px 10px; cursor: pointer; }
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
|
||||
.modal-content { background: white; margin: 100px auto; padding: 20px; width: 500px; }
|
||||
.form-group { margin: 10px 0; }
|
||||
label { display: block; margin-bottom: 5px; }
|
||||
input, select { width: 100%; padding: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Use Button Functionality</h1>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Discovered Endpoint</h3>
|
||||
<p><strong>Device:</strong> Modbus Controller</p>
|
||||
<p><strong>Address:</strong> 192.168.1.100:502</p>
|
||||
<p><strong>Protocol:</strong> modbus_tcp</p>
|
||||
<p><strong>Node:</strong> 40001</p>
|
||||
<button class="use-btn" onclick="useEndpoint('modbus_tcp', '40001', 'Modbus Controller', '192.168.1.100', '502')">Use</button>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<h3>Discovered Endpoint</h3>
|
||||
<p><strong>Device:</strong> OPC UA Server</p>
|
||||
<p><strong>Address:</strong> 192.168.1.101:4840</p>
|
||||
<p><strong>Protocol:</strong> opcua</p>
|
||||
<p><strong>Node:</strong> ns=2;s=Pressure</p>
|
||||
<button class="use-btn" onclick="useEndpoint('opcua', 'ns=2;s=Pressure', 'OPC UA Server', '192.168.1.101', '4840')">Use</button>
|
||||
</div>
|
||||
|
||||
<!-- Add New Mapping Modal -->
|
||||
<div id="addMappingModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Add New Protocol Mapping</h2>
|
||||
<form id="protocolMappingForm">
|
||||
<div class="form-group">
|
||||
<label for="mapping-id">Mapping ID:</label>
|
||||
<input type="text" id="mapping-id" name="mapping-id">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol-type">Protocol Type:</label>
|
||||
<select id="protocol-type" name="protocol-type">
|
||||
<option value="modbus_tcp">Modbus TCP</option>
|
||||
<option value="opcua">OPC UA</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol-address">Protocol Address:</label>
|
||||
<input type="text" id="protocol-address" name="protocol-address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="device-name">Device Name:</label>
|
||||
<input type="text" id="device-name" name="device-name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="device-address">Device Address:</label>
|
||||
<input type="text" id="device-address" name="device-address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="device-port">Device Port:</label>
|
||||
<input type="text" id="device-port" name="device-port">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="button" onclick="closeModal()">Cancel</button>
|
||||
<button type="submit">Save Mapping</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Function to open the Add New Mapping modal
|
||||
function showAddMappingModal() {
|
||||
document.getElementById('addMappingModal').style.display = 'block';
|
||||
}
|
||||
|
||||
// Function to close the modal
|
||||
function closeModal() {
|
||||
document.getElementById('addMappingModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Function to use an endpoint (simulates the Use button)
|
||||
function useEndpoint(protocolType, protocolAddress, deviceName, deviceAddress, devicePort) {
|
||||
// First, open the Add New Mapping modal
|
||||
showAddMappingModal();
|
||||
|
||||
// Wait a moment for the modal to open, then populate fields
|
||||
setTimeout(() => {
|
||||
// Generate a mapping ID
|
||||
const mappingId = `${protocolType}_${deviceName.replace(/\s+/g, '_').toLowerCase()}_${Date.now()}`;
|
||||
|
||||
// Populate form fields
|
||||
document.getElementById('mapping-id').value = mappingId;
|
||||
document.getElementById('protocol-type').value = protocolType;
|
||||
document.getElementById('protocol-address').value = protocolAddress;
|
||||
document.getElementById('device-name').value = deviceName;
|
||||
document.getElementById('device-address').value = deviceAddress;
|
||||
document.getElementById('device-port').value = devicePort;
|
||||
|
||||
// Show success message
|
||||
alert(`Protocol form populated with ${deviceName}. Please complete station, equipment, and data type information.`);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('addMappingModal');
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('protocolMappingForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
alert('Protocol mapping would be saved here!');
|
||||
closeModal();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,238 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Use Button Workflow</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.card { border: 1px solid #ddd; border-radius: 5px; padding: 20px; margin: 10px 0; }
|
||||
.success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; }
|
||||
.error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; }
|
||||
.info { background-color: #d1ecf1; border-color: #bee5eb; color: #0c5460; }
|
||||
button { padding: 10px 15px; margin: 5px; border: none; border-radius: 4px; cursor: pointer; }
|
||||
.btn-primary { background-color: #007bff; color: white; }
|
||||
.btn-success { background-color: #28a745; color: white; }
|
||||
.form-group { margin: 10px 0; }
|
||||
label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||
input, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Test Use Button Workflow</h1>
|
||||
<p>This page tests the "Use" button functionality with the new tag-based metadata system.</p>
|
||||
|
||||
<div class="card info">
|
||||
<h3>Step 1: Simulate Discovery Results</h3>
|
||||
<p>Click the button below to simulate discovering a device endpoint:</p>
|
||||
<button id="simulate-discovery" class="btn-primary">Simulate Discovery</button>
|
||||
</div>
|
||||
|
||||
<div class="card" id="discovery-results" style="display: none;">
|
||||
<h3>Discovery Results</h3>
|
||||
<div id="endpoint-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="protocol-form" style="display: none;">
|
||||
<h3>Protocol Mapping Form (Auto-populated)</h3>
|
||||
<form id="mapping-form">
|
||||
<div class="form-group">
|
||||
<label for="mapping-id">Mapping ID:</label>
|
||||
<input type="text" id="mapping-id" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol-type">Protocol Type:</label>
|
||||
<input type="text" id="protocol-type" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="protocol-address">Protocol Address:</label>
|
||||
<input type="text" id="protocol-address" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="station-id">Station ID:</label>
|
||||
<input type="text" id="station-id" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="equipment-id">Equipment ID:</label>
|
||||
<input type="text" id="equipment-id" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="data-type-id">Data Type ID:</label>
|
||||
<input type="text" id="data-type-id" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="db-source">Database Source:</label>
|
||||
<input type="text" id="db-source" value="pump_data.speed">
|
||||
</div>
|
||||
<button type="button" id="create-mapping" class="btn-success">Create Protocol Mapping</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" id="result-message" style="display: none;">
|
||||
<h3>Result</h3>
|
||||
<div id="result-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simulate discovery results
|
||||
document.getElementById('simulate-discovery').addEventListener('click', function() {
|
||||
const endpoints = [
|
||||
{
|
||||
device_id: 'device_001',
|
||||
protocol_type: 'modbus_tcp',
|
||||
device_name: 'Test Pump Controller',
|
||||
address: '192.168.1.100',
|
||||
port: 502,
|
||||
capabilities: ['read_holding_registers', 'write_holding_registers'],
|
||||
discovered_at: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
// Display discovery results
|
||||
const endpointList = document.getElementById('endpoint-list');
|
||||
endpointList.innerHTML = `
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="background-color: #f8f9fa;">
|
||||
<th style="padding: 8px; border: 1px solid #ddd;">Device Name</th>
|
||||
<th style="padding: 8px; border: 1px solid #ddd;">Protocol</th>
|
||||
<th style="padding: 8px; border: 1px solid #ddd;">Address</th>
|
||||
<th style="padding: 8px; border: 1px solid #ddd;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${endpoints.map(endpoint => `
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">${endpoint.device_name}</td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">${endpoint.protocol_type}</td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">${endpoint.address}:${endpoint.port}</td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">
|
||||
<button class="use-endpoint" data-endpoint='${JSON.stringify(endpoint)}'>Use</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
document.getElementById('discovery-results').style.display = 'block';
|
||||
|
||||
// Add event listeners to Use buttons
|
||||
document.querySelectorAll('.use-endpoint').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const endpoint = JSON.parse(this.getAttribute('data-endpoint'));
|
||||
populateProtocolForm(endpoint);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Populate protocol form with endpoint data
|
||||
function populateProtocolForm(endpoint) {
|
||||
const mappingId = `${endpoint.device_id}_${endpoint.protocol_type}`;
|
||||
|
||||
// Set form values
|
||||
document.getElementById('mapping-id').value = mappingId;
|
||||
document.getElementById('protocol-type').value = endpoint.protocol_type;
|
||||
document.getElementById('protocol-address').value = getDefaultProtocolAddress(endpoint);
|
||||
document.getElementById('station-id').value = 'station_001';
|
||||
document.getElementById('equipment-id').value = 'equipment_001';
|
||||
document.getElementById('data-type-id').value = 'datatype_001';
|
||||
|
||||
// Show the form
|
||||
document.getElementById('protocol-form').style.display = 'block';
|
||||
|
||||
// Scroll to form
|
||||
document.getElementById('protocol-form').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Get default protocol address
|
||||
function getDefaultProtocolAddress(endpoint) {
|
||||
switch (endpoint.protocol_type) {
|
||||
case 'modbus_tcp':
|
||||
return '40001';
|
||||
case 'opc_ua':
|
||||
return `ns=2;s=${endpoint.device_name.replace(/\s+/g, '_')}`;
|
||||
case 'rest_api':
|
||||
return `http://${endpoint.address}${endpoint.port ? ':' + endpoint.port : ''}/api/data`;
|
||||
default:
|
||||
return endpoint.address;
|
||||
}
|
||||
}
|
||||
|
||||
// Create protocol mapping
|
||||
document.getElementById('create-mapping').addEventListener('click', async function() {
|
||||
const formData = {
|
||||
protocol_type: document.getElementById('protocol-type').value,
|
||||
station_id: document.getElementById('station-id').value,
|
||||
equipment_id: document.getElementById('equipment-id').value,
|
||||
data_type_id: document.getElementById('data-type-id').value,
|
||||
protocol_address: document.getElementById('protocol-address').value,
|
||||
db_source: document.getElementById('db-source').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('http://95.111.206.155:8081/api/v1/dashboard/protocol-mappings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
const resultDiv = document.getElementById('result-content');
|
||||
const resultCard = document.getElementById('result-message');
|
||||
|
||||
if (response.ok && result.success) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="success">
|
||||
<h4>✅ Success!</h4>
|
||||
<p>Protocol mapping created successfully:</p>
|
||||
<ul>
|
||||
<li><strong>ID:</strong> ${result.mapping.id}</li>
|
||||
<li><strong>Station:</strong> ${result.mapping.station_id}</li>
|
||||
<li><strong>Equipment:</strong> ${result.mapping.equipment_id}</li>
|
||||
<li><strong>Data Type:</strong> ${result.mapping.data_type_id}</li>
|
||||
<li><strong>Protocol:</strong> ${result.mapping.protocol_type}</li>
|
||||
<li><strong>Address:</strong> ${result.mapping.protocol_address}</li>
|
||||
<li><strong>DB Source:</strong> ${result.mapping.db_source}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
resultCard.style.display = 'block';
|
||||
} else {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="error">
|
||||
<h4>❌ Error</h4>
|
||||
<p>Failed to create protocol mapping:</p>
|
||||
<p><strong>Status:</strong> ${response.status}</p>
|
||||
<p><strong>Error:</strong> ${result.detail || 'Unknown error'}</p>
|
||||
</div>
|
||||
`;
|
||||
resultCard.style.display = 'block';
|
||||
}
|
||||
|
||||
resultCard.scrollIntoView({ behavior: 'smooth' });
|
||||
|
||||
} catch (error) {
|
||||
const resultDiv = document.getElementById('result-content');
|
||||
const resultCard = document.getElementById('result-message');
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div class="error">
|
||||
<h4>❌ Network Error</h4>
|
||||
<p>Failed to connect to server:</p>
|
||||
<p><strong>Error:</strong> ${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
resultCard.style.display = 'block';
|
||||
resultCard.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue