/** * 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 = `
Discovery scan in progress... (Scan ID: ${status.current_scan_id})
`; scanButton?.setAttribute('disabled', 'true'); stopButton?.removeAttribute('disabled'); } else { statusElement.innerHTML = `
Discovery service ready
`; 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 = `
No endpoints discovered in this scan
`; return; } let html = `
Discovery Results (${endpoints.length} endpoints found)
`; 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 += ` `; }); html += `
Protocol Device Name Address Capabilities Discovered Actions
${protocolBadge} ${endpoint.device_name || 'Unknown Device'} ${endpoint.address}${endpoint.port ? ':' + endpoint.port : ''} ${capabilities} ${discoveredTime}
`; 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) { // First, open the "Add New Mapping" modal this.openAddMappingModal(); // Wait a moment for the modal to open, then populate fields setTimeout(() => { // Find and populate form fields in the modal 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'); if (protocolTypeField) protocolTypeField.value = formData.protocol_type; if (protocolAddressField) protocolAddressField.value = formData.protocol_address; // Set station, equipment, and data type if they exist in our metadata if (stationIdField && this.isValidStationId(formData.station_id)) { stationIdField.value = formData.station_id; // Trigger equipment dropdown update stationIdField.dispatchEvent(new Event('change')); } if (equipmentIdField && this.isValidEquipmentId(formData.equipment_id)) { equipmentIdField.value = formData.equipment_id; } if (dataTypeIdField && this.isValidDataTypeId(formData.data_type_id)) { dataTypeIdField.value = formData.data_type_id; } // 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() { // Look for the showAddMappingModal function or button click if (typeof showAddMappingModal === 'function') { showAddMappingModal(); } else { // Try to find and click the "Add New Mapping" button const addButton = document.querySelector('button[onclick*="showAddMappingModal"]'); if (addButton) { addButton.click(); } else { // 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': 'Modbus TCP', 'modbus_rtu': 'Modbus RTU', 'opc_ua': 'OPC UA', 'rest_api': 'REST API' }; return badges[protocolType] || `${protocolType}`; } /** * 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} `; 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); } } // Initialize discovery when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.protocolDiscovery = new ProtocolDiscovery(); window.protocolDiscovery.init(); });