2025-10-30 13:56:36 +00:00
|
|
|
#!/bin/bash
|
|
|
|
|
|
|
|
|
|
# Calejo Control Adapter - Test Environment Setup Script
|
|
|
|
|
# Sets up mock SCADA and optimizer services for testing
|
|
|
|
|
|
|
|
|
|
set -e
|
|
|
|
|
|
|
|
|
|
# Colors for output
|
|
|
|
|
RED='\033[0;31m'
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
YELLOW='\033[1;33m'
|
|
|
|
|
BLUE='\033[0;34m'
|
|
|
|
|
NC='\033[0m' # No Color
|
|
|
|
|
|
|
|
|
|
# Function to print colored output
|
|
|
|
|
print_status() {
|
|
|
|
|
echo -e "${BLUE}[INFO]${NC} $1"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
print_success() {
|
|
|
|
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
print_warning() {
|
|
|
|
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
print_error() {
|
|
|
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Function to display usage
|
|
|
|
|
usage() {
|
|
|
|
|
echo "Usage: $0 [options]"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Options:"
|
|
|
|
|
echo " --scada-only Only setup mock SCADA services"
|
|
|
|
|
echo " --optimizer-only Only setup mock optimizer services"
|
|
|
|
|
echo " --with-dashboard Include test dashboard setup"
|
|
|
|
|
echo " --clean Clean up existing test services"
|
|
|
|
|
echo " -h, --help Show this help message"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Examples:"
|
|
|
|
|
echo " $0 # Setup complete test environment"
|
|
|
|
|
echo " $0 --scada-only # Setup only mock SCADA services"
|
|
|
|
|
echo " $0 --clean # Clean up test environment"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Parse command line arguments
|
|
|
|
|
SCADA_ONLY=false
|
|
|
|
|
OPTIMIZER_ONLY=false
|
|
|
|
|
WITH_DASHBOARD=false
|
|
|
|
|
CLEAN=false
|
|
|
|
|
|
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
|
|
|
case $1 in
|
|
|
|
|
--scada-only)
|
|
|
|
|
SCADA_ONLY=true
|
|
|
|
|
shift
|
|
|
|
|
;;
|
|
|
|
|
--optimizer-only)
|
|
|
|
|
OPTIMIZER_ONLY=true
|
|
|
|
|
shift
|
|
|
|
|
;;
|
|
|
|
|
--with-dashboard)
|
|
|
|
|
WITH_DASHBOARD=true
|
|
|
|
|
shift
|
|
|
|
|
;;
|
|
|
|
|
--clean)
|
|
|
|
|
CLEAN=true
|
|
|
|
|
shift
|
|
|
|
|
;;
|
|
|
|
|
-h|--help)
|
|
|
|
|
usage
|
|
|
|
|
exit 0
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
print_error "Unknown option: $1"
|
|
|
|
|
usage
|
|
|
|
|
exit 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# Check if Docker is available
|
|
|
|
|
if ! command -v docker &> /dev/null; then
|
|
|
|
|
print_error "Docker is not installed or not in PATH"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if ! command -v docker-compose &> /dev/null; then
|
|
|
|
|
print_error "Docker Compose is not installed or not in PATH"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Function to cleanup test services
|
|
|
|
|
cleanup_test_services() {
|
|
|
|
|
print_status "Cleaning up test services..."
|
|
|
|
|
|
|
|
|
|
# Stop and remove test containers
|
|
|
|
|
docker-compose -f docker-compose.test.yml down --remove-orphans 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
# Remove test network if exists
|
|
|
|
|
docker network rm calejo-test-network 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
# Remove test volumes
|
|
|
|
|
docker volume rm calejo-scada-data 2>/dev/null || true
|
|
|
|
|
docker volume rm calejo-optimizer-data 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
print_success "Test services cleaned up"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# If clean mode, cleanup and exit
|
|
|
|
|
if [[ "$CLEAN" == "true" ]]; then
|
|
|
|
|
cleanup_test_services
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Create test docker-compose file
|
|
|
|
|
print_status "Creating test environment configuration..."
|
|
|
|
|
|
|
|
|
|
cat > docker-compose.test.yml << 'EOF'
|
|
|
|
|
version: '3.8'
|
|
|
|
|
|
|
|
|
|
services:
|
|
|
|
|
# Main Calejo Control Adapter
|
|
|
|
|
calejo-control-adapter:
|
|
|
|
|
build:
|
|
|
|
|
context: .
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
container_name: calejo-control-adapter-test
|
|
|
|
|
ports:
|
2025-11-01 17:50:28 +00:00
|
|
|
- "8081:8081" # REST API
|
2025-10-30 13:56:36 +00:00
|
|
|
- "4840:4840" # OPC UA
|
|
|
|
|
- "502:502" # Modbus TCP
|
|
|
|
|
- "9090:9090" # Prometheus metrics
|
|
|
|
|
environment:
|
|
|
|
|
- DATABASE_URL=postgresql://calejo:password@postgres:5432/calejo
|
|
|
|
|
- JWT_SECRET_KEY=test-secret-key
|
|
|
|
|
- API_KEY=test-api-key
|
|
|
|
|
- MOCK_SCADA_ENABLED=true
|
|
|
|
|
- MOCK_OPTIMIZER_ENABLED=true
|
|
|
|
|
- SCADA_MOCK_URL=http://mock-scada:8081
|
|
|
|
|
- OPTIMIZER_MOCK_URL=http://mock-optimizer:8082
|
|
|
|
|
depends_on:
|
|
|
|
|
- postgres
|
|
|
|
|
- mock-scada
|
|
|
|
|
- mock-optimizer
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
healthcheck:
|
2025-11-01 17:50:28 +00:00
|
|
|
test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
|
2025-10-30 13:56:36 +00:00
|
|
|
interval: 30s
|
|
|
|
|
timeout: 10s
|
|
|
|
|
retries: 3
|
|
|
|
|
start_period: 30s
|
|
|
|
|
volumes:
|
|
|
|
|
- ./logs:/app/logs
|
|
|
|
|
- ./config:/app/config
|
|
|
|
|
networks:
|
|
|
|
|
- calejo-test-network
|
|
|
|
|
|
|
|
|
|
# PostgreSQL Database
|
|
|
|
|
postgres:
|
|
|
|
|
image: postgres:15
|
|
|
|
|
container_name: calejo-postgres-test
|
|
|
|
|
environment:
|
|
|
|
|
- POSTGRES_DB=calejo
|
|
|
|
|
- POSTGRES_USER=calejo
|
|
|
|
|
- POSTGRES_PASSWORD=password
|
|
|
|
|
ports:
|
|
|
|
|
- "5432:5432"
|
|
|
|
|
volumes:
|
|
|
|
|
- postgres_data:/var/lib/postgresql/data
|
|
|
|
|
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks:
|
|
|
|
|
- calejo-test-network
|
|
|
|
|
|
|
|
|
|
# Mock SCADA System
|
|
|
|
|
mock-scada:
|
|
|
|
|
image: python:3.11-slim
|
|
|
|
|
container_name: calejo-mock-scada
|
|
|
|
|
ports:
|
|
|
|
|
- "8081:8081"
|
|
|
|
|
working_dir: /app
|
|
|
|
|
volumes:
|
|
|
|
|
- ./tests/mock_services:/app
|
|
|
|
|
command: >
|
|
|
|
|
sh -c "pip install flask requests &&
|
|
|
|
|
python mock_scada_server.py"
|
|
|
|
|
environment:
|
|
|
|
|
- FLASK_ENV=development
|
|
|
|
|
- PORT=8081
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
|
|
|
|
|
interval: 30s
|
|
|
|
|
timeout: 10s
|
|
|
|
|
retries: 3
|
|
|
|
|
networks:
|
|
|
|
|
- calejo-test-network
|
|
|
|
|
|
|
|
|
|
# Mock Optimizer Service
|
|
|
|
|
mock-optimizer:
|
|
|
|
|
image: python:3.11-slim
|
|
|
|
|
container_name: calejo-mock-optimizer
|
|
|
|
|
ports:
|
|
|
|
|
- "8082:8082"
|
|
|
|
|
working_dir: /app
|
|
|
|
|
volumes:
|
|
|
|
|
- ./tests/mock_services:/app
|
|
|
|
|
command: >
|
|
|
|
|
sh -c "pip install flask requests numpy &&
|
|
|
|
|
python mock_optimizer_server.py"
|
|
|
|
|
environment:
|
|
|
|
|
- FLASK_ENV=development
|
|
|
|
|
- PORT=8082
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD", "curl", "-f", "http://localhost:8082/health"]
|
|
|
|
|
interval: 30s
|
|
|
|
|
timeout: 10s
|
|
|
|
|
retries: 3
|
|
|
|
|
networks:
|
|
|
|
|
- calejo-test-network
|
|
|
|
|
|
|
|
|
|
# Test Data Generator
|
|
|
|
|
test-data-generator:
|
|
|
|
|
image: python:3.11-slim
|
|
|
|
|
container_name: calejo-test-data-generator
|
|
|
|
|
working_dir: /app
|
|
|
|
|
volumes:
|
|
|
|
|
- ./tests/mock_services:/app
|
|
|
|
|
command: >
|
|
|
|
|
sh -c "pip install requests &&
|
|
|
|
|
python test_data_generator.py"
|
|
|
|
|
depends_on:
|
|
|
|
|
- calejo-control-adapter
|
|
|
|
|
- mock-scada
|
|
|
|
|
restart: "no"
|
|
|
|
|
networks:
|
|
|
|
|
- calejo-test-network
|
|
|
|
|
|
|
|
|
|
volumes:
|
|
|
|
|
postgres_data:
|
|
|
|
|
|
|
|
|
|
networks:
|
|
|
|
|
calejo-test-network:
|
|
|
|
|
driver: bridge
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
print_success "Test configuration created"
|
|
|
|
|
|
|
|
|
|
# Create mock services directory
|
|
|
|
|
print_status "Creating mock services..."
|
|
|
|
|
mkdir -p tests/mock_services
|
|
|
|
|
|
|
|
|
|
# Create mock SCADA server
|
|
|
|
|
cat > tests/mock_services/mock_scada_server.py << 'EOF'
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
Mock SCADA Server for Testing
|
|
|
|
|
Simulates a real SCADA system with industrial process data
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import random
|
|
|
|
|
import time
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from flask import Flask, jsonify, request
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
|
|
|
|
# Mock SCADA data storage
|
|
|
|
|
scada_data = {
|
|
|
|
|
"temperature": {"value": 75.0, "unit": "°C", "min": 50.0, "max": 100.0},
|
|
|
|
|
"pressure": {"value": 15.2, "unit": "bar", "min": 10.0, "max": 20.0},
|
|
|
|
|
"flow_rate": {"value": 120.5, "unit": "m³/h", "min": 80.0, "max": 150.0},
|
|
|
|
|
"level": {"value": 65.3, "unit": "%", "min": 0.0, "max": 100.0},
|
|
|
|
|
"power": {"value": 450.7, "unit": "kW", "min": 300.0, "max": 600.0},
|
|
|
|
|
"status": {"value": "RUNNING", "options": ["STOPPED", "RUNNING", "FAULT"]},
|
|
|
|
|
"efficiency": {"value": 87.5, "unit": "%", "min": 0.0, "max": 100.0}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Equipment status
|
|
|
|
|
equipment_status = {
|
|
|
|
|
"pump_1": "RUNNING",
|
|
|
|
|
"pump_2": "STOPPED",
|
|
|
|
|
"valve_1": "OPEN",
|
|
|
|
|
"valve_2": "CLOSED",
|
|
|
|
|
"compressor": "RUNNING",
|
|
|
|
|
"heater": "ON"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
|
|
|
def health():
|
|
|
|
|
"""Health check endpoint"""
|
|
|
|
|
return jsonify({"status": "healthy", "service": "mock-scada"})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/v1/data', methods=['GET'])
|
|
|
|
|
def get_all_data():
|
|
|
|
|
"""Get all SCADA data"""
|
|
|
|
|
# Simulate data variation
|
|
|
|
|
for key in scada_data:
|
|
|
|
|
if key != "status":
|
|
|
|
|
current = scada_data[key]
|
|
|
|
|
variation = random.uniform(-2.0, 2.0)
|
|
|
|
|
new_value = current["value"] + variation
|
|
|
|
|
# Keep within bounds
|
|
|
|
|
new_value = max(current["min"], min(new_value, current["max"]))
|
|
|
|
|
scada_data[key]["value"] = round(new_value, 2)
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
|
|
|
"data": scada_data,
|
|
|
|
|
"equipment": equipment_status
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/v1/data/<tag>', methods=['GET'])
|
|
|
|
|
def get_specific_data(tag):
|
|
|
|
|
"""Get specific SCADA data tag"""
|
|
|
|
|
if tag in scada_data:
|
|
|
|
|
# Simulate variation for numeric values
|
|
|
|
|
if tag != "status":
|
|
|
|
|
current = scada_data[tag]
|
|
|
|
|
variation = random.uniform(-1.0, 1.0)
|
|
|
|
|
new_value = current["value"] + variation
|
|
|
|
|
new_value = max(current["min"], min(new_value, current["max"]))
|
|
|
|
|
scada_data[tag]["value"] = round(new_value, 2)
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"tag": tag,
|
|
|
|
|
"value": scada_data[tag]["value"],
|
|
|
|
|
"unit": scada_data[tag].get("unit", ""),
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
|
|
})
|
|
|
|
|
else:
|
|
|
|
|
return jsonify({"error": "Tag not found"}), 404
|
|
|
|
|
|
|
|
|
|
@app.route('/api/v1/control/<equipment>', methods=['POST'])
|
|
|
|
|
def control_equipment(equipment):
|
|
|
|
|
"""Control SCADA equipment"""
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data or 'command' not in data:
|
|
|
|
|
return jsonify({"error": "Missing command"}), 400
|
|
|
|
|
|
|
|
|
|
command = data['command']
|
|
|
|
|
|
|
|
|
|
if equipment in equipment_status:
|
|
|
|
|
# Simulate control logic
|
|
|
|
|
if command in ["START", "STOP", "OPEN", "CLOSE", "ON", "OFF"]:
|
|
|
|
|
old_status = equipment_status[equipment]
|
|
|
|
|
equipment_status[equipment] = command
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"equipment": equipment,
|
|
|
|
|
"previous_status": old_status,
|
|
|
|
|
"current_status": command,
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
|
|
|
"message": f"Equipment {equipment} changed from {old_status} to {command}"
|
|
|
|
|
})
|
|
|
|
|
else:
|
|
|
|
|
return jsonify({"error": "Invalid command"}), 400
|
|
|
|
|
else:
|
|
|
|
|
return jsonify({"error": "Equipment not found"}), 404
|
|
|
|
|
|
|
|
|
|
@app.route('/api/v1/alarms', methods=['GET'])
|
|
|
|
|
def get_alarms():
|
|
|
|
|
"""Get current alarms"""
|
|
|
|
|
# Simulate occasional alarms
|
|
|
|
|
alarms = []
|
|
|
|
|
|
|
|
|
|
# Temperature alarm
|
|
|
|
|
if scada_data["temperature"]["value"] > 90:
|
|
|
|
|
alarms.append({
|
|
|
|
|
"id": "TEMP_HIGH",
|
|
|
|
|
"message": "High temperature alarm",
|
|
|
|
|
"severity": "HIGH",
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Pressure alarm
|
|
|
|
|
if scada_data["pressure"]["value"] > 18:
|
|
|
|
|
alarms.append({
|
|
|
|
|
"id": "PRESS_HIGH",
|
|
|
|
|
"message": "High pressure alarm",
|
|
|
|
|
"severity": "MEDIUM",
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return jsonify({"alarms": alarms})
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
import os
|
|
|
|
|
port = int(os.getenv('PORT', 8081))
|
|
|
|
|
app.run(host='0.0.0.0', port=port, debug=True)
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
print_success "Mock SCADA server created"
|
|
|
|
|
|
|
|
|
|
# Create mock optimizer server
|
|
|
|
|
cat > tests/mock_services/mock_optimizer_server.py << 'EOF'
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
Mock Optimizer Server for Testing
|
|
|
|
|
Simulates an optimization service for industrial processes
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import random
|
|
|
|
|
import numpy as np
|
|
|
|
|
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 across processes",
|
|
|
|
|
"parameters": ["power_load", "time_of_day", "production_rate"]
|
|
|
|
|
},
|
|
|
|
|
"production_optimization": {
|
|
|
|
|
"name": "Production Efficiency Optimizer",
|
|
|
|
|
"description": "Maximizes production efficiency",
|
|
|
|
|
"parameters": ["raw_material_quality", "machine_utilization", "operator_skill"]
|
|
|
|
|
},
|
|
|
|
|
"cost_optimization": {
|
|
|
|
|
"name": "Cost Reduction Optimizer",
|
|
|
|
|
"description": "Minimizes operational costs",
|
|
|
|
|
"parameters": ["energy_cost", "labor_cost", "maintenance_cost"]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Optimization history
|
|
|
|
|
optimization_history = []
|
|
|
|
|
|
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
|
|
|
def health():
|
|
|
|
|
"""Health check endpoint"""
|
|
|
|
|
return jsonify({"status": "healthy", "service": "mock-optimizer"})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/v1/models', methods=['GET'])
|
|
|
|
|
def get_models():
|
|
|
|
|
"""Get available optimization models"""
|
|
|
|
|
return jsonify({"models": optimization_models})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/v1/optimize/<model_name>', methods=['POST'])
|
|
|
|
|
def optimize(model_name):
|
|
|
|
|
"""Run optimization for a specific model"""
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data:
|
|
|
|
|
return jsonify({"error": "No input data provided"}), 400
|
|
|
|
|
|
|
|
|
|
if model_name not in optimization_models:
|
|
|
|
|
return jsonify({"error": "Model not found"}), 404
|
|
|
|
|
|
|
|
|
|
# Simulate optimization processing
|
|
|
|
|
processing_time = random.uniform(0.5, 3.0)
|
|
|
|
|
|
|
|
|
|
# Generate optimization results
|
|
|
|
|
if model_name == "energy_optimization":
|
|
|
|
|
result = {
|
|
|
|
|
"optimal_power_setpoint": random.uniform(400, 500),
|
|
|
|
|
"recommended_actions": [
|
|
|
|
|
"Reduce compressor load during peak hours",
|
|
|
|
|
"Optimize pump sequencing",
|
|
|
|
|
"Adjust temperature setpoints"
|
|
|
|
|
],
|
|
|
|
|
"estimated_savings": random.uniform(5, 15),
|
|
|
|
|
"confidence": random.uniform(0.7, 0.95)
|
|
|
|
|
}
|
|
|
|
|
elif model_name == "production_optimization":
|
|
|
|
|
result = {
|
|
|
|
|
"optimal_production_rate": random.uniform(80, 120),
|
|
|
|
|
"recommended_actions": [
|
|
|
|
|
"Adjust raw material mix",
|
|
|
|
|
"Optimize machine speeds",
|
|
|
|
|
"Improve operator scheduling"
|
|
|
|
|
],
|
|
|
|
|
"efficiency_gain": random.uniform(3, 12),
|
|
|
|
|
"confidence": random.uniform(0.75, 0.92)
|
|
|
|
|
}
|
|
|
|
|
elif model_name == "cost_optimization":
|
|
|
|
|
result = {
|
|
|
|
|
"optimal_cost_structure": {
|
|
|
|
|
"energy": random.uniform(40, 60),
|
|
|
|
|
"labor": random.uniform(25, 35),
|
|
|
|
|
"maintenance": random.uniform(10, 20)
|
|
|
|
|
},
|
|
|
|
|
"recommended_actions": [
|
|
|
|
|
"Shift energy consumption to off-peak",
|
|
|
|
|
"Optimize maintenance schedules",
|
|
|
|
|
"Improve labor allocation"
|
|
|
|
|
],
|
|
|
|
|
"cost_reduction": random.uniform(8, 20),
|
|
|
|
|
"confidence": random.uniform(0.8, 0.98)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Record optimization
|
|
|
|
|
optimization_record = {
|
|
|
|
|
"model": model_name,
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
|
|
|
"input_data": data,
|
|
|
|
|
"result": result,
|
|
|
|
|
"processing_time": processing_time
|
|
|
|
|
}
|
|
|
|
|
optimization_history.append(optimization_record)
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"optimization_id": len(optimization_history),
|
|
|
|
|
"model": model_name,
|
|
|
|
|
"result": result,
|
|
|
|
|
"processing_time": processing_time,
|
|
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/v1/history', methods=['GET'])
|
|
|
|
|
def get_history():
|
|
|
|
|
"""Get optimization history"""
|
|
|
|
|
limit = request.args.get('limit', 10, type=int)
|
|
|
|
|
return jsonify({
|
|
|
|
|
"history": optimization_history[-limit:],
|
|
|
|
|
"total_optimizations": len(optimization_history)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/v1/forecast', methods=['POST'])
|
|
|
|
|
def forecast():
|
|
|
|
|
"""Generate forecasts based on current data"""
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data or 'hours' not in data:
|
|
|
|
|
return jsonify({"error": "Missing forecast hours"}), 400
|
|
|
|
|
|
|
|
|
|
hours = data['hours']
|
|
|
|
|
|
|
|
|
|
# Generate mock forecast
|
|
|
|
|
forecast_data = []
|
|
|
|
|
current_time = datetime.utcnow()
|
|
|
|
|
|
|
|
|
|
for i in range(hours):
|
|
|
|
|
forecast_time = current_time + timedelta(hours=i)
|
|
|
|
|
forecast_data.append({
|
|
|
|
|
"timestamp": forecast_time.isoformat(),
|
|
|
|
|
"energy_consumption": random.uniform(400, 600),
|
|
|
|
|
"production_rate": random.uniform(85, 115),
|
|
|
|
|
"efficiency": random.uniform(80, 95),
|
|
|
|
|
"cost": random.uniform(45, 65)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"forecast": forecast_data,
|
|
|
|
|
"generated_at": current_time.isoformat(),
|
|
|
|
|
"horizon_hours": hours
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
import os
|
|
|
|
|
port = int(os.getenv('PORT', 8082))
|
|
|
|
|
app.run(host='0.0.0.0', port=port, debug=True)
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
print_success "Mock optimizer server created"
|
|
|
|
|
|
|
|
|
|
# Create test data generator
|
|
|
|
|
cat > tests/mock_services/test_data_generator.py << 'EOF'
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
Test Data Generator
|
|
|
|
|
Generates realistic test data for the Calejo Control Adapter
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
import time
|
|
|
|
|
import random
|
|
|
|
|
import json
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
# Configuration
|
|
|
|
|
CALEJO_API_URL = "http://calejo-control-adapter-test:8080"
|
|
|
|
|
SCADA_MOCK_URL = "http://mock-scada:8081"
|
|
|
|
|
OPTIMIZER_MOCK_URL = "http://mock-optimizer:8082"
|
|
|
|
|
|
|
|
|
|
# Test scenarios
|
|
|
|
|
test_scenarios = [
|
|
|
|
|
"normal_operation",
|
|
|
|
|
"high_load",
|
|
|
|
|
"low_efficiency",
|
|
|
|
|
"alarm_condition",
|
|
|
|
|
"optimization_test"
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def test_health_checks():
|
|
|
|
|
"""Test health of all services"""
|
|
|
|
|
print("🔍 Testing service health...")
|
|
|
|
|
|
|
|
|
|
services = [
|
|
|
|
|
("Calejo Control Adapter", f"{CALEJO_API_URL}/health"),
|
|
|
|
|
("Mock SCADA", f"{SCADA_MOCK_URL}/health"),
|
|
|
|
|
("Mock Optimizer", f"{OPTIMIZER_MOCK_URL}/health")
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for service_name, url in services:
|
|
|
|
|
try:
|
|
|
|
|
response = requests.get(url, timeout=5)
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
print(f" ✅ {service_name}: Healthy")
|
|
|
|
|
else:
|
|
|
|
|
print(f" ❌ {service_name}: Unhealthy (Status: {response.status_code})")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f" ❌ {service_name}: Connection failed - {e}")
|
|
|
|
|
|
|
|
|
|
def generate_scada_data():
|
|
|
|
|
"""Generate and send SCADA data"""
|
|
|
|
|
print("📊 Generating SCADA test data...")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Get current SCADA data
|
|
|
|
|
response = requests.get(f"{SCADA_MOCK_URL}/api/v1/data")
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
data = response.json()
|
|
|
|
|
print(f" 📈 Current SCADA data: {len(data.get('data', {}))} tags")
|
|
|
|
|
|
|
|
|
|
# Send some control commands
|
|
|
|
|
equipment_to_control = ["pump_1", "valve_1", "compressor"]
|
|
|
|
|
for equipment in equipment_to_control:
|
|
|
|
|
command = random.choice(["START", "STOP", "OPEN", "CLOSE"])
|
|
|
|
|
try:
|
|
|
|
|
control_response = requests.post(
|
|
|
|
|
f"{SCADA_MOCK_URL}/api/v1/control/{equipment}",
|
|
|
|
|
json={"command": command},
|
|
|
|
|
timeout=5
|
|
|
|
|
)
|
|
|
|
|
if control_response.status_code == 200:
|
|
|
|
|
print(f" 🎛️ Controlled {equipment}: {command}")
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f" ❌ SCADA data generation failed: {e}")
|
|
|
|
|
|
|
|
|
|
def test_optimization():
|
|
|
|
|
"""Test optimization scenarios"""
|
|
|
|
|
print("🧠 Testing optimization...")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Get available models
|
|
|
|
|
response = requests.get(f"{OPTIMIZER_MOCK_URL}/api/v1/models")
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
models = response.json().get('models', {})
|
|
|
|
|
|
|
|
|
|
# Test each model
|
|
|
|
|
for model_name in models:
|
|
|
|
|
test_data = {
|
|
|
|
|
"power_load": random.uniform(400, 600),
|
|
|
|
|
"time_of_day": random.randint(0, 23),
|
|
|
|
|
"production_rate": random.uniform(80, 120)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opt_response = requests.post(
|
|
|
|
|
f"{OPTIMIZER_MOCK_URL}/api/v1/optimize/{model_name}",
|
|
|
|
|
json=test_data,
|
|
|
|
|
timeout=10
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if opt_response.status_code == 200:
|
|
|
|
|
result = opt_response.json()
|
|
|
|
|
print(f" ✅ {model_name}: Optimization completed")
|
|
|
|
|
print(f" Processing time: {result.get('processing_time', 0):.2f}s")
|
|
|
|
|
else:
|
|
|
|
|
print(f" ❌ {model_name}: Optimization failed")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f" ❌ Optimization test failed: {e}")
|
|
|
|
|
|
|
|
|
|
def test_calejo_api():
|
|
|
|
|
"""Test Calejo Control Adapter API"""
|
|
|
|
|
print("🌐 Testing Calejo API...")
|
|
|
|
|
|
|
|
|
|
endpoints = [
|
|
|
|
|
"/health",
|
|
|
|
|
"/dashboard",
|
|
|
|
|
"/api/v1/status",
|
|
|
|
|
"/api/v1/metrics"
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for endpoint in endpoints:
|
|
|
|
|
try:
|
|
|
|
|
response = requests.get(f"{CALEJO_API_URL}{endpoint}", timeout=5)
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
print(f" ✅ {endpoint}: Accessible")
|
|
|
|
|
else:
|
|
|
|
|
print(f" ⚠️ {endpoint}: Status {response.status_code}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f" ❌ {endpoint}: Failed - {e}")
|
|
|
|
|
|
|
|
|
|
def run_comprehensive_test():
|
|
|
|
|
"""Run comprehensive test scenario"""
|
|
|
|
|
print("\n🚀 Starting comprehensive test scenario...")
|
|
|
|
|
print("=" * 50)
|
|
|
|
|
|
|
|
|
|
# Test all components
|
|
|
|
|
test_health_checks()
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
generate_scada_data()
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
test_optimization()
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
test_calejo_api()
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
print("✅ Comprehensive test completed!")
|
|
|
|
|
print("\n📋 Test Summary:")
|
|
|
|
|
print(" • Service health checks")
|
|
|
|
|
print(" • SCADA data generation and control")
|
|
|
|
|
print(" • Optimization model testing")
|
|
|
|
|
print(" • Calejo API endpoint validation")
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
# Wait a bit for services to start
|
|
|
|
|
print("⏳ Waiting for services to initialize...")
|
|
|
|
|
time.sleep(10)
|
|
|
|
|
|
|
|
|
|
run_comprehensive_test()
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
print_success "Test data generator created"
|
|
|
|
|
|
|
|
|
|
# Start test services
|
|
|
|
|
print_status "Starting test services..."
|
|
|
|
|
|
|
|
|
|
docker-compose -f docker-compose.test.yml up -d
|
|
|
|
|
|
|
|
|
|
# Wait for services to start
|
|
|
|
|
print_status "Waiting for services to initialize..."
|
|
|
|
|
sleep 30
|
|
|
|
|
|
|
|
|
|
# Run health checks
|
|
|
|
|
print_status "Running health checks..."
|
|
|
|
|
|
|
|
|
|
# Check Calejo Control Adapter
|
2025-11-01 17:50:28 +00:00
|
|
|
if curl -f http://localhost:8081/health > /dev/null 2>&1; then
|
2025-10-30 13:56:36 +00:00
|
|
|
print_success "Calejo Control Adapter is healthy"
|
|
|
|
|
else
|
|
|
|
|
print_error "Calejo Control Adapter health check failed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Check Mock SCADA
|
|
|
|
|
if curl -f http://localhost:8081/health > /dev/null 2>&1; then
|
|
|
|
|
print_success "Mock SCADA is healthy"
|
|
|
|
|
else
|
|
|
|
|
print_error "Mock SCADA health check failed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Check Mock Optimizer
|
|
|
|
|
if curl -f http://localhost:8082/health > /dev/null 2>&1; then
|
|
|
|
|
print_success "Mock Optimizer is healthy"
|
|
|
|
|
else
|
|
|
|
|
print_error "Mock Optimizer health check failed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Run test data generator
|
|
|
|
|
print_status "Running test data generator..."
|
|
|
|
|
docker-compose -f docker-compose.test.yml run --rm test-data-generator
|
|
|
|
|
|
|
|
|
|
print_success "Test environment setup completed!"
|
|
|
|
|
|
|
|
|
|
# Display access information
|
|
|
|
|
print ""
|
|
|
|
|
echo "=================================================="
|
|
|
|
|
echo " TEST ENVIRONMENT READY"
|
|
|
|
|
echo "=================================================="
|
|
|
|
|
echo ""
|
|
|
|
|
echo "🌐 Access URLs:"
|
2025-11-01 17:50:28 +00:00
|
|
|
echo " Calejo Dashboard: http://localhost:8081/dashboard"
|
2025-10-30 13:56:36 +00:00
|
|
|
echo " Mock SCADA API: http://localhost:8081/api/v1/data"
|
|
|
|
|
echo " Mock Optimizer API: http://localhost:8082/api/v1/models"
|
|
|
|
|
echo " PostgreSQL: localhost:5432"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "🔧 Management Commands:"
|
|
|
|
|
echo " View logs: docker-compose -f docker-compose.test.yml logs -f"
|
|
|
|
|
echo " Stop services: docker-compose -f docker-compose.test.yml down"
|
|
|
|
|
echo " Cleanup: ./scripts/setup-test-environment.sh --clean"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "🧪 Test Commands:"
|
|
|
|
|
echo " Run tests: python -m pytest tests/"
|
|
|
|
|
echo " Generate data: docker-compose -f docker-compose.test.yml run --rm test-data-generator"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=================================================="
|