CalejoControl/static/discovery.js

762 lines
28 KiB
JavaScript
Raw Normal View History

/**
* Protocol Discovery JavaScript
* Handles auto-discovery of protocol endpoints and integration with protocol mapping
*/
class ProtocolDiscovery {
constructor() {
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() {
// 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);
}
});
}
/**
* Start a new discovery scan
*/
async startDiscoveryScan() {
try {
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('Error starting discovery scan:', error);
this.showNotification(`Failed to start discovery scan: ${error.message}`, 'error');
this.setScanningState(false);
}
}
/**
* 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');
}
/**
* 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
</div>
`;
scanButton?.removeAttribute('disabled');
stopButton?.setAttribute('disabled', 'true');
}
}
/**
* Display discovery results
*/
displayDiscoveryResults(result) {
const resultsContainer = document.getElementById('discovery-results');
if (!resultsContainer) return;
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="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>
</div>
`;
resultsContainer.innerHTML = html;
// Re-bind apply button
document.getElementById('apply-discovery-results')?.addEventListener('click', () => {
this.applyDiscoveryResults();
});
}
/**
* Apply discovery results as protocol mappings
*/
async applyDiscoveryResults() {
if (!this.currentScanId) {
this.showNotification('No discovery results to apply', 'warning');
return;
}
// Get station, equipment, and data type from our metadata
const stationId = this.getDefaultStationId();
const equipmentId = this.getDefaultEquipmentId(stationId);
const dataTypeId = this.getDefaultDataTypeId();
const dbSource = 'influxdb'; // Default database source
try {
const response = await fetch(`/api/v1/dashboard/discovery/apply/${this.currentScanId}?station_id=${stationId}&equipment_id=${equipmentId}&data_type_id=${dataTypeId}&db_source=${dbSource}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
if (result.created_mappings.length > 0) {
this.showNotification(`Successfully created ${result.created_mappings.length} protocol mappings from discovery results`, 'success');
// Refresh protocol mappings grid
if (window.loadProtocolMappings) {
window.loadProtocolMappings();
}
} else {
this.showNotification('No protocol mappings were created. Check the discovery results for compatible endpoints.', 'warning');
}
} else {
throw new Error(result.detail || 'Failed to apply discovery results');
}
} catch (error) {
console.error('Error applying discovery results:', error);
this.showNotification(`Failed to apply discovery results: ${error.message}`, 'error');
}
}
/**
* Use discovered endpoint in protocol form
*/
async useDiscoveredEndpoint(endpointId) {
try {
// Get current scan results to find the endpoint
if (!this.currentScanId) {
this.showNotification('No discovery results available', 'warning');
return;
}
const response = await fetch(`/api/v1/dashboard/discovery/results/${this.currentScanId}`);
const result = await response.json();
if (!result.success) {
throw new Error('Failed to fetch discovery results');
}
// Find the specific endpoint
const endpoint = result.discovered_endpoints.find(ep => ep.device_id === endpointId);
if (!endpoint) {
this.showNotification(`Endpoint ${endpointId} not found in current scan`, 'warning');
return;
}
// Populate protocol mapping form with endpoint data
this.populateProtocolForm(endpoint);
// Switch to protocol mapping tab
this.switchToProtocolMappingTab();
this.showNotification(`Endpoint ${endpoint.device_name || endpointId} selected for protocol mapping`, 'success');
} catch (error) {
console.error('Error using discovered endpoint:', error);
this.showNotification(`Failed to use endpoint: ${error.message}`, 'error');
}
}
/**
* Populate protocol mapping form with endpoint data
*/
populateProtocolForm(endpoint) {
// Create a new protocol mapping ID
const mappingId = `${endpoint.device_id}_${endpoint.protocol_type}`;
// Get default metadata IDs from our sample metadata
const defaultStationId = this.getDefaultStationId();
const defaultEquipmentId = this.getDefaultEquipmentId(defaultStationId);
const defaultDataTypeId = this.getDefaultDataTypeId();
// Set form values (these would be used when creating a new mapping)
const formData = {
mapping_id: mappingId,
protocol_type: endpoint.protocol_type === 'opc_ua' ? 'opcua' : endpoint.protocol_type,
protocol_address: this.getDefaultProtocolAddress(endpoint),
device_name: endpoint.device_name || endpoint.device_id,
device_address: endpoint.address,
device_port: endpoint.port || '',
station_id: defaultStationId,
equipment_id: defaultEquipmentId,
data_type_id: defaultDataTypeId
};
// Store form data for later use
this.selectedEndpoint = formData;
// Show form data in console for debugging
console.log('Protocol form populated with:', formData);
// Auto-populate the protocol mapping form
this.autoPopulateProtocolForm(formData);
}
/**
* Auto-populate the protocol mapping form with endpoint data
*/
autoPopulateProtocolForm(formData) {
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');
this.showNotification('Could not open protocol mapping form. Please try opening it manually.', 'error');
}
}, 2000);
}
/**
* Populate modal fields with discovery data
*/
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 they exist in our metadata
if (stationIdField) {
// Wait for stations to be loaded if needed
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, '_');
}
// Show success message
this.showNotification(`Protocol form populated with ${formData.device_name}. Please review and complete any missing information.`, 'success');
}, 100);
}
});
}
}
/**
* Open the "Add New Mapping" modal
*/
openAddMappingModal() {
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;
}
// Try to find and click the "Add New Mapping" button
const addButton = document.querySelector('button[onclick*="showAddMappingModal"]');
if (addButton) {
console.log('Found Add New Mapping button, clicking it');
addButton.click();
return;
}
// Try to find any button that might open the modal
const buttons = document.querySelectorAll('button');
for (let button of buttons) {
const text = button.textContent.toLowerCase();
if (text.includes('add') && text.includes('mapping')) {
console.log('Found Add Mapping button by text, clicking it');
button.click();
return;
}
}
// Last resort: try to show the modal directly
const modal = document.getElementById('mapping-modal');
if (modal) {
console.log('Found mapping-modal, showing it directly');
modal.style.display = 'block';
return;
}
console.error('Could not find any way to open the protocol mapping modal');
// Fallback: show a message to manually open the modal
this.showNotification('Please click "Add New Mapping" to create a protocol mapping with the discovered endpoint data.', 'info');
}
/**
* Get default protocol address based on endpoint type
*/
getDefaultProtocolAddress(endpoint) {
const protocolType = endpoint.protocol_type;
const deviceName = endpoint.device_name || endpoint.device_id;
switch (protocolType) {
case 'modbus_tcp':
return '40001'; // Default holding register
case 'opc_ua':
return `ns=2;s=${deviceName.replace(/\s+/g, '_')}`;
case 'rest_api':
return `http://${endpoint.address}${endpoint.port ? ':' + endpoint.port : ''}/api/data`;
default:
return endpoint.address;
}
}
/**
* Switch to protocol mapping tab
*/
switchToProtocolMappingTab() {
// Find and click the protocol mapping tab
const mappingTab = document.querySelector('[data-tab="protocol-mapping"]');
if (mappingTab) {
mappingTab.click();
} else {
// Fallback: scroll to protocol mapping section
const mappingSection = document.querySelector('#protocol-mapping-section');
if (mappingSection) {
mappingSection.scrollIntoView({ behavior: 'smooth' });
}
}
// Show guidance message
this.showNotification('Please complete the protocol mapping form with station, pump, and data type information', 'info');
}
/**
* 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');
}
}
/**
* 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 = `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) {
notification.remove();
}
}, 5000);
}
/**
* Get default station ID from available metadata
*/
getDefaultStationId() {
// Try to get the first station from our metadata
const stationSelect = document.getElementById('station_id');
if (stationSelect && stationSelect.options.length > 1) {
return stationSelect.options[1].value; // First actual station (skip "Select Station")
}
// Fallback to our sample metadata IDs
return 'station_main';
}
/**
* Get default equipment ID for a station
*/
getDefaultEquipmentId(stationId) {
// Try to get the first equipment for the station
const equipmentSelect = document.getElementById('equipment_id');
if (equipmentSelect && equipmentSelect.options.length > 1) {
return equipmentSelect.options[1].value; // First actual equipment
}
// Fallback based on station
if (stationId === 'station_main') return 'pump_primary';
if (stationId === 'station_backup') return 'pump_backup';
if (stationId === 'station_control') return 'controller_plc';
return 'pump_primary';
}
/**
* Get default data type ID
*/
getDefaultDataTypeId() {
// Try to get the first data type from our metadata
const dataTypeSelect = document.getElementById('data_type_id');
if (dataTypeSelect && dataTypeSelect.options.length > 1) {
return dataTypeSelect.options[1].value; // First actual data type
}
// Fallback to our sample metadata IDs
return 'speed_pump';
}
/**
* Check if station ID exists in our metadata
*/
isValidStationId(stationId) {
const stationSelect = document.getElementById('station_id');
if (!stationSelect) return false;
return Array.from(stationSelect.options).some(option => option.value === stationId);
}
/**
* Check if equipment ID exists in our metadata
*/
isValidEquipmentId(equipmentId) {
const equipmentSelect = document.getElementById('equipment_id');
if (!equipmentSelect) return false;
return Array.from(equipmentSelect.options).some(option => option.value === equipmentId);
}
/**
* Check if data type ID exists in our metadata
*/
isValidDataTypeId(dataTypeId) {
const dataTypeSelect = document.getElementById('data_type_id');
if (!dataTypeSelect) return false;
return Array.from(dataTypeSelect.options).some(option => option.value === dataTypeId);
}
/**
* Wait for stations to be loaded in the dropdown
*/
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 (more than just "Select Station")
if (stationSelect.options.length > 1) {
console.log('Stations already loaded:', stationSelect.options.length);
callback();
return;
}
// Wait for stations to load
const startTime = Date.now();
const checkInterval = setInterval(() => {
if (stationSelect.options.length > 1) {
console.log('Stations loaded after wait:', stationSelect.options.length);
clearInterval(checkInterval);
callback();
} else if (Date.now() - startTime > maxWait) {
console.warn('Timeout waiting for stations to load');
clearInterval(checkInterval);
callback();
}
}, 100);
}
}
// Initialize discovery when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.protocolDiscovery = new ProtocolDiscovery();
window.protocolDiscovery.init();
});