diff --git a/scripts/test-mock-services-standalone.py b/scripts/test-mock-services-standalone.py new file mode 100755 index 0000000..a41b301 --- /dev/null +++ b/scripts/test-mock-services-standalone.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +""" +Standalone test script for mock SCADA and optimizer services +This script can test the services without requiring Docker +""" + +import subprocess +import sys +import time +import requests +import json +from datetime import datetime + +def run_command(cmd, check=True): + """Run a shell command and return output""" + try: + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=check) + return result.stdout, result.stderr, result.returncode + except subprocess.CalledProcessError as e: + return e.stdout, e.stderr, e.returncode + +def check_python_dependencies(): + """Check if required Python packages are installed""" + required_packages = ['flask', 'requests'] + missing = [] + + for package in required_packages: + try: + __import__(package) + except ImportError: + missing.append(package) + + if missing: + print(f"โŒ Missing required packages: {', '.join(missing)}") + print("Install with: pip install flask requests") + return False + + print("โœ… All required Python packages are installed") + return True + +def start_mock_services(): + """Start the mock services in background""" + print("๐Ÿš€ Starting mock services...") + + # Start SCADA service + scada_cmd = "cd tests/mock_services && python mock_scada_server.py > /tmp/scada.log 2>&1 &" + stdout, stderr, code = run_command(scada_cmd) + + # Start Optimizer service + optimizer_cmd = "cd tests/mock_services && python mock_optimizer_server.py > /tmp/optimizer.log 2>&1 &" + stdout, stderr, code = run_command(optimizer_cmd) + + print("โœ… Mock services started in background") + print(" SCADA logs: /tmp/scada.log") + print(" Optimizer logs: /tmp/optimizer.log") + +def wait_for_services(): + """Wait for services to be ready""" + print("โณ Waiting for services to be ready...") + + max_wait = 30 + start_time = time.time() + + while time.time() - start_time < max_wait: + try: + scada_ready = requests.get("http://localhost:8081/health", timeout=2).status_code == 200 + optimizer_ready = requests.get("http://localhost:8082/health", timeout=2).status_code == 200 + + if scada_ready and optimizer_ready: + print("โœ… All services are ready!") + return True + except: + pass + + print(f" Waiting... ({int(time.time() - start_time)}/{max_wait} seconds)") + time.sleep(2) + + print("โŒ Services not ready within timeout period") + return False + +def test_scada_service(): + """Test SCADA service functionality""" + print("\n๐Ÿ“Š Testing SCADA Service...") + + tests_passed = 0 + total_tests = 0 + + try: + # Test health endpoint + total_tests += 1 + response = requests.get("http://localhost:8081/health") + if response.status_code == 200 and response.json().get("status") == "healthy": + print(" โœ… Health check passed") + tests_passed += 1 + else: + print(" โŒ Health check failed") + + # Test data endpoint + total_tests += 1 + response = requests.get("http://localhost:8081/api/v1/data") + if response.status_code == 200: + data = response.json() + if "data" in data and "equipment" in data: + print(" โœ… Data retrieval passed") + tests_passed += 1 + else: + print(" โŒ Data structure invalid") + else: + print(" โŒ Data endpoint failed") + + # Test specific data tag + total_tests += 1 + response = requests.get("http://localhost:8081/api/v1/data/temperature") + if response.status_code == 200: + data = response.json() + if "value" in data and "unit" in data: + print(" โœ… Specific data tag passed") + tests_passed += 1 + else: + print(" โŒ Specific data structure invalid") + else: + print(" โŒ Specific data endpoint failed") + + # Test equipment control + total_tests += 1 + response = requests.post( + "http://localhost:8081/api/v1/control/pump_1", + json={"command": "START"} + ) + if response.status_code == 200: + data = response.json() + if "current_status" in data and data["current_status"] == "START": + print(" โœ… Equipment control passed") + tests_passed += 1 + else: + print(" โŒ Equipment control response invalid") + else: + print(" โŒ Equipment control failed") + + # Test alarms + total_tests += 1 + response = requests.get("http://localhost:8081/api/v1/alarms") + if response.status_code == 200: + data = response.json() + if "alarms" in data: + print(" โœ… Alarms endpoint passed") + tests_passed += 1 + else: + print(" โŒ Alarms structure invalid") + else: + print(" โŒ Alarms endpoint failed") + + except Exception as e: + print(f" โŒ SCADA test error: {e}") + + print(f" ๐Ÿ“ˆ SCADA tests: {tests_passed}/{total_tests} passed") + return tests_passed, total_tests + +def test_optimizer_service(): + """Test optimizer service functionality""" + print("\n๐Ÿง  Testing Optimizer Service...") + + tests_passed = 0 + total_tests = 0 + + try: + # Test health endpoint + total_tests += 1 + response = requests.get("http://localhost:8082/health") + if response.status_code == 200 and response.json().get("status") == "healthy": + print(" โœ… Health check passed") + tests_passed += 1 + else: + print(" โŒ Health check failed") + + # Test models endpoint + total_tests += 1 + response = requests.get("http://localhost:8082/api/v1/models") + if response.status_code == 200: + data = response.json() + if "models" in data and "energy_optimization" in data["models"]: + print(" โœ… Models endpoint passed") + tests_passed += 1 + else: + print(" โŒ Models structure invalid") + else: + print(" โŒ Models endpoint failed") + + # Test energy optimization + total_tests += 1 + response = requests.post( + "http://localhost:8082/api/v1/optimize/energy_optimization", + json={"power_load": 450, "time_of_day": 14, "production_rate": 95} + ) + if response.status_code == 200: + data = response.json() + if "result" in data and "optimization_id" in data: + print(" โœ… Energy optimization passed") + tests_passed += 1 + else: + print(" โŒ Optimization response invalid") + else: + print(" โŒ Energy optimization failed") + + # Test forecast + total_tests += 1 + response = requests.post( + "http://localhost:8082/api/v1/forecast", + json={"hours": 12} + ) + if response.status_code == 200: + data = response.json() + if "forecast" in data and len(data["forecast"]) == 12: + print(" โœ… Forecast passed") + tests_passed += 1 + else: + print(" โŒ Forecast structure invalid") + else: + print(" โŒ Forecast failed") + + # Test history + total_tests += 1 + response = requests.get("http://localhost:8082/api/v1/history") + if response.status_code == 200: + data = response.json() + if "history" in data and "total_optimizations" in data: + print(" โœ… History endpoint passed") + tests_passed += 1 + else: + print(" โŒ History structure invalid") + else: + print(" โŒ History endpoint failed") + + except Exception as e: + print(f" โŒ Optimizer test error: {e}") + + print(f" ๐Ÿ“ˆ Optimizer tests: {tests_passed}/{total_tests} passed") + return tests_passed, total_tests + +def test_end_to_end_workflow(): + """Test end-to-end workflow""" + print("\n๐Ÿ”„ Testing End-to-End Workflow...") + + tests_passed = 0 + total_tests = 0 + + try: + # Get SCADA data + total_tests += 1 + scada_response = requests.get("http://localhost:8081/api/v1/data") + if scada_response.status_code == 200: + scada_data = scada_response.json() + power_value = scada_data["data"]["power"]["value"] + print(" โœ… SCADA data retrieved") + tests_passed += 1 + else: + print(" โŒ SCADA data retrieval failed") + return tests_passed, total_tests + + # Run optimization based on SCADA data + total_tests += 1 + opt_response = requests.post( + "http://localhost:8082/api/v1/optimize/energy_optimization", + json={ + "power_load": power_value, + "time_of_day": datetime.now().hour, + "production_rate": 95 + } + ) + if opt_response.status_code == 200: + opt_data = opt_response.json() + if "result" in opt_data and "recommended_actions" in opt_data["result"]: + print(" โœ… Optimization based on SCADA data passed") + tests_passed += 1 + else: + print(" โŒ Optimization response invalid") + else: + print(" โŒ Optimization failed") + + # Test control based on optimization + total_tests += 1 + control_response = requests.post( + "http://localhost:8081/api/v1/control/compressor", + json={"command": "START"} + ) + if control_response.status_code == 200: + control_data = control_response.json() + if "current_status" in control_data: + print(" โœ… Control based on workflow passed") + tests_passed += 1 + else: + print(" โŒ Control response invalid") + else: + print(" โŒ Control failed") + + except Exception as e: + print(f" โŒ End-to-end test error: {e}") + + print(f" ๐Ÿ“ˆ End-to-end tests: {tests_passed}/{total_tests} passed") + return tests_passed, total_tests + +def stop_services(): + """Stop the mock services""" + print("\n๐Ÿ›‘ Stopping mock services...") + + # Find and kill the processes + run_command("pkill -f 'python mock_scada_server.py'", check=False) + run_command("pkill -f 'python mock_optimizer_server.py'", check=False) + + print("โœ… Mock services stopped") + +def main(): + """Main test runner""" + print("๐Ÿงช Standalone Mock Services Test Runner") + print("=" * 50) + + # Check dependencies + if not check_python_dependencies(): + sys.exit(1) + + # Create mock services directory if needed + import os + os.makedirs("tests/mock_services", exist_ok=True) + + # Check if mock service files exist + if not os.path.exists("tests/mock_services/mock_scada_server.py"): + print("โŒ Mock service files not found. Run setup script first:") + print(" ./scripts/setup-test-environment.sh") + sys.exit(1) + + # Start services + start_mock_services() + + # Wait for services + if not wait_for_services(): + stop_services() + sys.exit(1) + + # Run tests + total_passed = 0 + total_tests = 0 + + # Test SCADA + passed, tests = test_scada_service() + total_passed += passed + total_tests += tests + + # Test Optimizer + passed, tests = test_optimizer_service() + total_passed += passed + total_tests += tests + + # Test End-to-End + passed, tests = test_end_to_end_workflow() + total_passed += passed + total_tests += tests + + # Stop services + stop_services() + + # Print summary + print("\n" + "=" * 50) + print("๐Ÿ“Š TEST SUMMARY") + print("=" * 50) + print(f"Total Tests: {total_tests}") + print(f"Tests Passed: {total_passed}") + print(f"Tests Failed: {total_tests - total_passed}") + print(f"Success Rate: {(total_passed/total_tests)*100:.1f}%") + + if total_passed == total_tests: + print("\n๐ŸŽ‰ ALL TESTS PASSED!") + print("Mock services are working correctly!") + else: + print(f"\nโŒ {total_tests - total_passed} TESTS FAILED") + print("Check the logs above for details") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/mock_services/mock_optimizer_server.py b/tests/mock_services/mock_optimizer_server.py new file mode 100644 index 0000000..c253641 --- /dev/null +++ b/tests/mock_services/mock_optimizer_server.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +""" +Mock Optimizer Server for testing +Simulates optimization service for industrial processes +""" + +import json +import random +import time +from datetime import datetime, timedelta +from flask import Flask, jsonify, request + +app = Flask(__name__) + +# Mock optimization models +optimization_models = { + "energy_optimization": { + "name": "Energy Consumption Optimizer", + "description": "Optimizes energy usage while maintaining production targets", + "parameters": ["power_load", "time_of_day", "production_rate"], + "outputs": ["optimal_power_setpoint", "recommended_actions", "estimated_savings", "confidence"] + }, + "production_optimization": { + "name": "Production Efficiency Optimizer", + "description": "Maximizes production efficiency and throughput", + "parameters": ["raw_material_quality", "machine_utilization", "operator_skill"], + "outputs": ["optimal_production_rate", "efficiency_gain", "recommended_adjustments"] + }, + "cost_optimization": { + "name": "Cost Reduction Optimizer", + "description": "Minimizes operational costs while maintaining quality", + "parameters": ["energy_cost", "labor_cost", "maintenance_cost"], + "outputs": ["optimal_cost_structure", "cost_reduction", "implementation_plan"] + } +} + +# Optimization history +optimization_history = [] + +def generate_optimization_id(): + """Generate unique optimization ID""" + return f"OPT_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{random.randint(1000, 9999)}" + +def run_energy_optimization(data): + """Run energy optimization""" + power_load = data.get("power_load", 450.0) + time_of_day = data.get("time_of_day", 12) + production_rate = data.get("production_rate", 95.0) + + # Mock optimization logic + if time_of_day >= 6 and time_of_day <= 18: + # Daytime optimization + optimal_power = power_load * 0.85 # Reduce power during peak hours + savings = power_load * 0.15 * 0.12 # 15% reduction at $0.12/kWh + else: + # Nighttime optimization + optimal_power = power_load * 0.95 # Slight reduction at night + savings = power_load * 0.05 * 0.08 # 5% reduction at $0.08/kWh + + # Adjust based on production rate + if production_rate < 80: + optimal_power *= 0.9 + savings *= 1.1 + elif production_rate > 95: + optimal_power *= 1.05 + savings *= 0.9 + + return { + "optimal_power_setpoint": round(optimal_power, 2), + "recommended_actions": [ + "Adjust compressor speed to 85%", + "Optimize pump sequencing", + "Implement variable frequency drive" + ], + "estimated_savings": round(savings, 2), + "confidence": random.uniform(0.85, 0.95) + } + +def run_production_optimization(data): + """Run production optimization""" + raw_material_quality = data.get("raw_material_quality", 85.0) + machine_utilization = data.get("machine_utilization", 92.0) + operator_skill = data.get("operator_skill", 88.0) + + # Mock optimization logic + base_rate = 100.0 + + # Adjust based on factors + quality_factor = raw_material_quality / 100.0 + utilization_factor = machine_utilization / 100.0 + skill_factor = operator_skill / 100.0 + + optimal_rate = base_rate * quality_factor * utilization_factor * skill_factor + efficiency_gain = (optimal_rate - base_rate) / base_rate * 100 + + return { + "optimal_production_rate": round(optimal_rate, 2), + "efficiency_gain": round(efficiency_gain, 2), + "recommended_adjustments": [ + "Increase raw material inspection frequency", + "Optimize machine maintenance schedule", + "Provide operator training on efficiency techniques" + ] + } + +def run_cost_optimization(data): + """Run cost optimization""" + energy_cost = data.get("energy_cost", 55.0) + labor_cost = data.get("labor_cost", 30.0) + maintenance_cost = data.get("maintenance_cost", 15.0) + + total_cost = energy_cost + labor_cost + maintenance_cost + + # Mock optimization logic + optimal_energy = energy_cost * 0.9 # 10% reduction + optimal_labor = labor_cost * 0.95 # 5% reduction + optimal_maintenance = maintenance_cost * 1.05 # 5% increase for better maintenance + + optimal_total = optimal_energy + optimal_labor + optimal_maintenance + cost_reduction = total_cost - optimal_total + + return { + "optimal_cost_structure": { + "energy": round(optimal_energy, 2), + "labor": round(optimal_labor, 2), + "maintenance": round(optimal_maintenance, 2), + "total": round(optimal_total, 2) + }, + "cost_reduction": round(cost_reduction, 2), + "implementation_plan": [ + "Implement energy monitoring system", + "Optimize workforce scheduling", + "Increase preventive maintenance frequency" + ] + } + +def generate_forecast(hours=24): + """Generate forecast data""" + forecast = [] + base_time = datetime.now() + + for i in range(hours): + timestamp = base_time + timedelta(hours=i) + hour = timestamp.hour + + # Generate realistic forecast data + if hour >= 6 and hour <= 18: + # Daytime values + energy_consumption = random.uniform(450, 550) + production_rate = random.uniform(95, 105) + efficiency = random.uniform(85, 92) + cost = random.uniform(50, 60) + else: + # Nighttime values + energy_consumption = random.uniform(350, 450) + production_rate = random.uniform(80, 95) + efficiency = random.uniform(78, 85) + cost = random.uniform(40, 50) + + forecast.append({ + "timestamp": timestamp.isoformat(), + "energy_consumption": round(energy_consumption, 2), + "production_rate": round(production_rate, 2), + "efficiency": round(efficiency, 2), + "cost": round(cost, 2) + }) + + return forecast + +@app.route('/health', methods=['GET']) +def health(): + """Health check endpoint""" + return jsonify({ + "status": "healthy", + "service": "mock-optimizer", + "timestamp": datetime.now().isoformat(), + "models_available": len(optimization_models) + }) + +@app.route('/api/v1/models', methods=['GET']) +def get_models(): + """Get available optimization models""" + return jsonify({ + "models": optimization_models, + "timestamp": datetime.now().isoformat() + }) + +@app.route('/api/v1/optimize/', methods=['POST']) +def run_optimization(model_name): + """Run optimization with specified model""" + if model_name not in optimization_models: + return jsonify({"error": f"Model '{model_name}' not found"}), 404 + + data = request.json + if not data: + return jsonify({"error": "No data provided"}), 400 + + start_time = time.time() + + # Run appropriate optimization + if model_name == "energy_optimization": + result = run_energy_optimization(data) + elif model_name == "production_optimization": + result = run_production_optimization(data) + elif model_name == "cost_optimization": + result = run_cost_optimization(data) + else: + return jsonify({"error": "Model not implemented"}), 501 + + processing_time = round(time.time() - start_time, 3) + optimization_id = generate_optimization_id() + + # Add to history + optimization_history.append({ + "optimization_id": optimization_id, + "model": model_name, + "timestamp": datetime.now().isoformat(), + "processing_time": processing_time + }) + + return jsonify({ + "optimization_id": optimization_id, + "model": model_name, + "result": result, + "processing_time": processing_time, + "timestamp": datetime.now().isoformat() + }) + +@app.route('/api/v1/history', methods=['GET']) +def get_history(): + """Get optimization history""" + return jsonify({ + "history": optimization_history[-10:], # Last 10 optimizations + "total_optimizations": len(optimization_history), + "timestamp": datetime.now().isoformat() + }) + +@app.route('/api/v1/forecast', methods=['POST']) +def generate_optimization_forecast(): + """Generate optimization forecast""" + data = request.json or {} + hours = data.get("hours", 24) + + if hours > 168: # Max 1 week + return jsonify({"error": "Forecast horizon too long. Maximum 168 hours (1 week)"}), 400 + + forecast = generate_forecast(hours) + + return jsonify({ + "forecast": forecast, + "generated_at": datetime.now().isoformat(), + "horizon_hours": hours + }) + +@app.route('/api/v1/status/', methods=['GET']) +def get_optimization_status(optimization_id): + """Get status of specific optimization""" + for opt in optimization_history: + if opt["optimization_id"] == optimization_id: + return jsonify({ + "optimization_id": optimization_id, + "status": "completed", + "model": opt["model"], + "completed_at": opt["timestamp"], + "processing_time": opt["processing_time"] + }) + + return jsonify({"error": f"Optimization '{optimization_id}' not found"}), 404 + +if __name__ == '__main__': + print("๐Ÿง  Starting Mock Optimizer Server on port 8082...") + print("๐Ÿ“Š Available endpoints:") + print(" GET /health") + print(" GET /api/v1/models") + print(" POST /api/v1/optimize/") + print(" GET /api/v1/history") + print(" POST /api/v1/forecast") + print(" GET /api/v1/status/") + + app.run(host='0.0.0.0', port=8082, debug=False) \ No newline at end of file diff --git a/tests/mock_services/mock_scada_server.py b/tests/mock_services/mock_scada_server.py new file mode 100644 index 0000000..13d75d1 --- /dev/null +++ b/tests/mock_services/mock_scada_server.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +Mock SCADA Server for testing +Simulates industrial SCADA system with process data and equipment control +""" + +import json +import random +import time +from datetime import datetime +from flask import Flask, jsonify, request + +app = Flask(__name__) + +# Mock SCADA data +current_data = { + "temperature": {"value": 75.5, "unit": "ยฐC", "min": 50, "max": 100}, + "pressure": {"value": 2.5, "unit": "bar", "min": 1, "max": 5}, + "flow_rate": {"value": 120.0, "unit": "mยณ/h", "min": 50, "max": 200}, + "level": {"value": 65.0, "unit": "%", "min": 0, "max": 100}, + "power": {"value": 450.0, "unit": "kW", "min": 200, "max": 600}, + "status": {"value": "RUNNING", "unit": "-"}, + "efficiency": {"value": 87.5, "unit": "%", "min": 0, "max": 100} +} + +# Equipment status +equipment_status = { + "pump_1": "RUNNING", + "pump_2": "STOPPED", + "valve_1": "OPEN", + "valve_2": "CLOSED", + "compressor": "RUNNING", + "heater": "STOPPED" +} + +# Alarm status +alarms = [] + +def update_data(): + """Update mock data with realistic variations""" + global current_data + + # Temperature variation + current_data["temperature"]["value"] += random.uniform(-0.5, 0.5) + current_data["temperature"]["value"] = max(50, min(100, current_data["temperature"]["value"])) + + # Pressure variation + current_data["pressure"]["value"] += random.uniform(-0.1, 0.1) + current_data["pressure"]["value"] = max(1, min(5, current_data["pressure"]["value"])) + + # Flow rate variation + current_data["flow_rate"]["value"] += random.uniform(-2, 2) + current_data["flow_rate"]["value"] = max(50, min(200, current_data["flow_rate"]["value"])) + + # Level variation + current_data["level"]["value"] += random.uniform(-1, 1) + current_data["level"]["value"] = max(0, min(100, current_data["level"]["value"])) + + # Power variation + current_data["power"]["value"] += random.uniform(-5, 5) + current_data["power"]["value"] = max(200, min(600, current_data["power"]["value"])) + + # Efficiency variation + current_data["efficiency"]["value"] += random.uniform(-0.5, 0.5) + current_data["efficiency"]["value"] = max(0, min(100, current_data["efficiency"]["value"])) + + # Check for alarm conditions + check_alarms() + +def check_alarms(): + """Check for alarm conditions""" + global alarms + + # Clear old alarms + alarms = [alarm for alarm in alarms if datetime.now().timestamp() - alarm["timestamp"] < 300] + + # Check temperature alarm + if current_data["temperature"]["value"] > 95: + add_alarm("HIGH_TEMPERATURE", f"Temperature high: {current_data['temperature']['value']:.1f}ยฐC") + elif current_data["temperature"]["value"] < 55: + add_alarm("LOW_TEMPERATURE", f"Temperature low: {current_data['temperature']['value']:.1f}ยฐC") + + # Check pressure alarm + if current_data["pressure"]["value"] > 4.5: + add_alarm("HIGH_PRESSURE", f"Pressure high: {current_data['pressure']['value']:.1f} bar") + elif current_data["pressure"]["value"] < 1.5: + add_alarm("LOW_PRESSURE", f"Pressure low: {current_data['pressure']['value']:.1f} bar") + + # Check level alarm + if current_data["level"]["value"] > 90: + add_alarm("HIGH_LEVEL", f"Level high: {current_data['level']['value']:.1f}%") + elif current_data["level"]["value"] < 20: + add_alarm("LOW_LEVEL", f"Level low: {current_data['level']['value']:.1f}%") + +def add_alarm(type, message): + """Add a new alarm""" + global alarms + + # Check if alarm already exists + for alarm in alarms: + if alarm["type"] == type: + return + + alarms.append({ + "type": type, + "message": message, + "timestamp": datetime.now().timestamp(), + "acknowledged": False + }) + +@app.route('/health', methods=['GET']) +def health(): + """Health check endpoint""" + return jsonify({ + "status": "healthy", + "service": "mock-scada", + "timestamp": datetime.now().isoformat() + }) + +@app.route('/api/v1/data', methods=['GET']) +def get_all_data(): + """Get all SCADA data""" + update_data() + + return jsonify({ + "timestamp": datetime.now().isoformat(), + "data": current_data, + "equipment": equipment_status + }) + +@app.route('/api/v1/data/', methods=['GET']) +def get_specific_data(tag): + """Get specific SCADA data tag""" + update_data() + + if tag not in current_data: + return jsonify({"error": f"Tag '{tag}' not found"}), 404 + + return jsonify({ + "tag": tag, + "value": current_data[tag]["value"], + "unit": current_data[tag]["unit"], + "timestamp": datetime.now().isoformat() + }) + +@app.route('/api/v1/control/', methods=['POST']) +def control_equipment(equipment): + """Control SCADA equipment""" + if equipment not in equipment_status: + return jsonify({"error": f"Equipment '{equipment}' not found"}), 404 + + command = request.json.get("command") + if not command: + return jsonify({"error": "Command is required"}), 400 + + valid_commands = ["START", "STOP", "OPEN", "CLOSE", "RESET"] + if command not in valid_commands: + return jsonify({"error": f"Invalid command. Valid commands: {valid_commands}"}), 400 + + previous_status = equipment_status[equipment] + equipment_status[equipment] = command + + return jsonify({ + "equipment": equipment, + "previous_status": previous_status, + "current_status": command, + "timestamp": datetime.now().isoformat(), + "message": f"Equipment {equipment} changed from {previous_status} to {command}" + }) + +@app.route('/api/v1/alarms', methods=['GET']) +def get_alarms(): + """Get current alarms""" + return jsonify({ + "alarms": alarms, + "timestamp": datetime.now().isoformat() + }) + +@app.route('/api/v1/alarms//acknowledge', methods=['POST']) +def acknowledge_alarm(alarm_type): + """Acknowledge an alarm""" + for alarm in alarms: + if alarm["type"] == alarm_type: + alarm["acknowledged"] = True + return jsonify({ + "alarm": alarm_type, + "acknowledged": True, + "timestamp": datetime.now().isoformat() + }) + + return jsonify({"error": f"Alarm '{alarm_type}' not found"}), 404 + +if __name__ == '__main__': + print("๐Ÿš€ Starting Mock SCADA Server on port 8081...") + print("๐Ÿ“Š Available endpoints:") + print(" GET /health") + print(" GET /api/v1/data") + print(" GET /api/v1/data/") + print(" POST /api/v1/control/") + print(" GET /api/v1/alarms") + print(" POST /api/v1/alarms//acknowledge") + + app.run(host='0.0.0.0', port=8081, debug=False) \ No newline at end of file