first commit
This commit is contained in:
422
microservices/DEPLOYMENT_GUIDE.md
Normal file
422
microservices/DEPLOYMENT_GUIDE.md
Normal file
@@ -0,0 +1,422 @@
|
||||
# Energy Management Microservices Deployment Guide
|
||||
|
||||
This guide provides comprehensive instructions for deploying and managing the Energy Management microservices architecture based on the tiocps/iot-building-monitoring system.
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
|
||||
The system consists of 6 independent microservices coordinated by an API Gateway:
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Client Apps │ │ Web Dashboard │ │ Mobile App │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
└───────────────────────┼───────────────────────┘
|
||||
│
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ API Gateway (Port 8000) │
|
||||
│ • Request routing │
|
||||
│ • Authentication │
|
||||
│ • Load balancing │
|
||||
│ • Rate limiting │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────┼───────────────────────────┐
|
||||
│ │ │
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Token │ │ Battery │ │ Demand │ │ P2P │ │Forecast │ │ IoT │
|
||||
│Service │ │Service │ │Response │ │Trading │ │Service │ │Control │
|
||||
│ 8001 │ │ 8002 │ │ 8003 │ │ 8004 │ │ 8005 │ │ 8006 │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
│ │ │ │ │ │
|
||||
└────────────┼────────────┼────────────┼────────────┼────────────┘
|
||||
│ │ │ │
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Shared Infrastructure │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ MongoDB │ │ Redis │ │
|
||||
│ │ :27017 │ │ :6379 │ │
|
||||
│ │ • Data │ │ • Caching │ │
|
||||
│ │ • Metadata │ │ • Events │ │
|
||||
│ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Docker 20.0+
|
||||
- Docker Compose 2.0+
|
||||
- 8GB RAM minimum
|
||||
- 10GB free disk space
|
||||
|
||||
### 1. Deploy the Complete System
|
||||
```bash
|
||||
cd microservices/
|
||||
./deploy.sh deploy
|
||||
```
|
||||
|
||||
This command will:
|
||||
- ✅ Check dependencies
|
||||
- ✅ Set up environment
|
||||
- ✅ Build all services
|
||||
- ✅ Start infrastructure (MongoDB, Redis)
|
||||
- ✅ Start all microservices
|
||||
- ✅ Configure networking
|
||||
- ✅ Run health checks
|
||||
|
||||
### 2. Verify Deployment
|
||||
```bash
|
||||
./deploy.sh status
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
[SUCCESS] api-gateway is healthy
|
||||
[SUCCESS] token-service is healthy
|
||||
[SUCCESS] battery-service is healthy
|
||||
[SUCCESS] demand-response-service is healthy
|
||||
[SUCCESS] p2p-trading-service is healthy
|
||||
[SUCCESS] forecasting-service is healthy
|
||||
[SUCCESS] iot-control-service is healthy
|
||||
```
|
||||
|
||||
### 3. Access the System
|
||||
- **API Gateway**: http://localhost:8000
|
||||
- **System Health**: http://localhost:8000/health
|
||||
- **Service Status**: http://localhost:8000/services/status
|
||||
- **System Overview**: http://localhost:8000/api/v1/overview
|
||||
|
||||
## 📋 Service Details
|
||||
|
||||
### 🔐 Token Service (Port 8001)
|
||||
**Purpose**: JWT authentication and authorization
|
||||
**Database**: `energy_dashboard_tokens`
|
||||
|
||||
**Key Endpoints**:
|
||||
```
|
||||
# Generate token
|
||||
curl -X POST "http://localhost:8001/tokens/generate" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "admin_user",
|
||||
"list_of_resources": ["batteries", "demand_response", "p2p", "forecasting", "iot"],
|
||||
"data_aggregation": true,
|
||||
"time_aggregation": true,
|
||||
"exp_hours": 24
|
||||
}'
|
||||
|
||||
# Validate token
|
||||
curl -X POST "http://localhost:8001/tokens/validate" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token": "your_jwt_token_here"}'
|
||||
```
|
||||
|
||||
### 🔋 Battery Service (Port 8002)
|
||||
**Purpose**: Energy storage management and optimization
|
||||
**Database**: `energy_dashboard_batteries`
|
||||
|
||||
**Key Features**:
|
||||
- Battery monitoring and status tracking
|
||||
- Charging/discharging control
|
||||
- Health monitoring and maintenance alerts
|
||||
- Energy storage optimization
|
||||
- Performance analytics
|
||||
|
||||
**Example Usage**:
|
||||
```bash
|
||||
# Get all batteries
|
||||
curl "http://localhost:8002/batteries" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# Charge a battery
|
||||
curl -X POST "http://localhost:8002/batteries/BATT001/charge" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"power_kw": 50.0,
|
||||
"duration_minutes": 120
|
||||
}'
|
||||
|
||||
# Get battery analytics
|
||||
curl "http://localhost:8002/batteries/analytics/summary" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
### ⚡ Demand Response Service (Port 8003)
|
||||
**Purpose**: Grid interaction and load management
|
||||
**Database**: `energy_dashboard_demand_response`
|
||||
|
||||
**Key Features**:
|
||||
- Demand response event management
|
||||
- Load reduction coordination
|
||||
- Flexibility forecasting
|
||||
- Auto-response configuration
|
||||
- Performance analytics
|
||||
|
||||
**Example Usage**:
|
||||
```bash
|
||||
# Send demand response invitation
|
||||
curl -X POST "http://localhost:8003/invitations/send" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"event_time": "2025-01-10T14:00:00Z",
|
||||
"load_kwh": 100,
|
||||
"load_percentage": 15,
|
||||
"duration_minutes": 60,
|
||||
"iots": ["DEVICE001", "DEVICE002"]
|
||||
}'
|
||||
|
||||
# Get current flexibility
|
||||
curl "http://localhost:8003/flexibility/current" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
### 🤝 P2P Trading Service (Port 8004)
|
||||
**Purpose**: Peer-to-peer energy marketplace
|
||||
**Database**: `energy_dashboard_p2p`
|
||||
|
||||
**Key Features**:
|
||||
- Energy trading marketplace
|
||||
- Bid/ask management
|
||||
- Transaction processing
|
||||
- Price optimization
|
||||
- Market analytics
|
||||
|
||||
### 📊 Forecasting Service (Port 8005)
|
||||
**Purpose**: ML-based energy forecasting
|
||||
**Database**: `energy_dashboard_forecasting`
|
||||
|
||||
**Key Features**:
|
||||
- Consumption forecasting
|
||||
- Generation forecasting
|
||||
- Flexibility forecasting
|
||||
- Historical data analysis
|
||||
- Model training and optimization
|
||||
|
||||
### 🏠 IoT Control Service (Port 8006)
|
||||
**Purpose**: IoT device management and control
|
||||
**Database**: `energy_dashboard_iot`
|
||||
|
||||
**Key Features**:
|
||||
- Device registration and management
|
||||
- Remote device control
|
||||
- Automation rules
|
||||
- Device status monitoring
|
||||
- Integration with other services
|
||||
|
||||
## 🛠️ Management Commands
|
||||
|
||||
### Service Management
|
||||
```bash
|
||||
# Start all services
|
||||
./deploy.sh start
|
||||
|
||||
# Stop all services
|
||||
./deploy.sh stop
|
||||
|
||||
# Restart all services
|
||||
./deploy.sh restart
|
||||
|
||||
# View service status
|
||||
./deploy.sh status
|
||||
```
|
||||
|
||||
### Logs and Debugging
|
||||
```bash
|
||||
# View all logs
|
||||
./deploy.sh logs
|
||||
|
||||
# View specific service logs
|
||||
./deploy.sh logs battery-service
|
||||
./deploy.sh logs api-gateway
|
||||
|
||||
# Follow logs in real-time
|
||||
docker-compose logs -f token-service
|
||||
```
|
||||
|
||||
### Scaling Services
|
||||
```bash
|
||||
# Scale a specific service
|
||||
docker-compose up -d --scale battery-service=3
|
||||
|
||||
# Scale multiple services
|
||||
docker-compose up -d \
|
||||
--scale battery-service=2 \
|
||||
--scale demand-response-service=2
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Environment Variables
|
||||
Each service can be configured using environment variables:
|
||||
|
||||
**Common Variables**:
|
||||
- `MONGO_URL`: MongoDB connection string
|
||||
- `REDIS_URL`: Redis connection string
|
||||
- `LOG_LEVEL`: Logging level (DEBUG, INFO, WARNING, ERROR)
|
||||
|
||||
**Service-Specific Variables**:
|
||||
- `JWT_SECRET_KEY`: Token service secret key
|
||||
- `TOKEN_SERVICE_URL`: API Gateway token service URL
|
||||
- `BATTERY_SERVICE_URL`: Battery service URL for IoT control
|
||||
|
||||
### Database Configuration
|
||||
MongoDB databases are automatically created:
|
||||
- `energy_dashboard_tokens`: Token management
|
||||
- `energy_dashboard_batteries`: Battery data
|
||||
- `energy_dashboard_demand_response`: DR events
|
||||
- `energy_dashboard_p2p`: P2P transactions
|
||||
- `energy_dashboard_forecasting`: Forecasting data
|
||||
- `energy_dashboard_iot`: IoT device data
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
### Authentication Flow
|
||||
1. Client requests token from Token Service
|
||||
2. Token Service validates credentials and issues JWT
|
||||
3. Client includes JWT in Authorization header
|
||||
4. API Gateway validates token with Token Service
|
||||
5. Request forwarded to target microservice
|
||||
|
||||
### Token Permissions
|
||||
Tokens include resource-based permissions:
|
||||
```json
|
||||
{
|
||||
"name": "user_name",
|
||||
"list_of_resources": ["batteries", "demand_response"],
|
||||
"data_aggregation": true,
|
||||
"time_aggregation": false,
|
||||
"embargo": 0,
|
||||
"exp": 1736524800
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Health Checks
|
||||
All services provide health endpoints:
|
||||
```bash
|
||||
# API Gateway health (includes all services)
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# Individual service health
|
||||
curl http://localhost:8001/health # Token Service
|
||||
curl http://localhost:8002/health # Battery Service
|
||||
curl http://localhost:8003/health # Demand Response Service
|
||||
```
|
||||
|
||||
### Metrics and Analytics
|
||||
- **Gateway Stats**: Request counts, success rates, uptime
|
||||
- **Battery Analytics**: Energy flows, efficiency, health
|
||||
- **DR Performance**: Event success rates, load reduction
|
||||
- **P2P Metrics**: Trading volumes, prices, participants
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Services won't start**:
|
||||
```bash
|
||||
# Check Docker status
|
||||
docker ps
|
||||
|
||||
# Check logs
|
||||
./deploy.sh logs
|
||||
|
||||
# Restart problematic service
|
||||
docker-compose restart battery-service
|
||||
```
|
||||
|
||||
**Database connection issues**:
|
||||
```bash
|
||||
# Check MongoDB status
|
||||
docker-compose logs mongodb
|
||||
|
||||
# Restart database
|
||||
docker-compose restart mongodb
|
||||
|
||||
# Wait for services to reconnect (30 seconds)
|
||||
```
|
||||
|
||||
**Authentication failures**:
|
||||
```bash
|
||||
# Check token service
|
||||
curl http://localhost:8001/health
|
||||
|
||||
# Verify token generation
|
||||
curl -X POST "http://localhost:8001/tokens/generate" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "test", "list_of_resources": ["test"]}'
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
- Increase service replicas for high load
|
||||
- Monitor memory usage and adjust limits
|
||||
- Use Redis for caching frequently accessed data
|
||||
- Implement database indexes for query optimization
|
||||
|
||||
## 🔄 Updates and Maintenance
|
||||
|
||||
### Service Updates
|
||||
```bash
|
||||
# Update specific service
|
||||
docker-compose build battery-service
|
||||
docker-compose up -d battery-service
|
||||
|
||||
# Update all services
|
||||
./deploy.sh build
|
||||
./deploy.sh restart
|
||||
```
|
||||
|
||||
### Database Maintenance
|
||||
```bash
|
||||
# Backup databases
|
||||
docker exec energy-mongodb mongodump --out /data/backup
|
||||
|
||||
# Restore databases
|
||||
docker exec energy-mongodb mongorestore /data/backup
|
||||
```
|
||||
|
||||
### Clean Deployment
|
||||
```bash
|
||||
# Complete system cleanup
|
||||
./deploy.sh cleanup
|
||||
|
||||
# Fresh deployment
|
||||
./deploy.sh deploy
|
||||
```
|
||||
|
||||
## 📈 Scaling and Production
|
||||
|
||||
### Production Considerations
|
||||
1. **Security**: Change default passwords and secrets
|
||||
2. **SSL/TLS**: Configure HTTPS with proper certificates
|
||||
3. **Monitoring**: Set up Prometheus and Grafana
|
||||
4. **Logging**: Configure centralized logging
|
||||
5. **Backup**: Implement automated database backups
|
||||
6. **Resource Limits**: Set appropriate CPU and memory limits
|
||||
|
||||
### Kubernetes Deployment
|
||||
The microservices can be deployed to Kubernetes:
|
||||
```bash
|
||||
# Generate Kubernetes manifests
|
||||
kompose convert
|
||||
|
||||
# Deploy to Kubernetes
|
||||
kubectl apply -f kubernetes/
|
||||
```
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
### Documentation
|
||||
- API documentation: http://localhost:8000/docs
|
||||
- Service-specific docs: http://localhost:800X/docs (where X = service port)
|
||||
|
||||
### Logs Location
|
||||
- Container logs: `docker-compose logs [service]`
|
||||
- Application logs: Check service-specific log files
|
||||
- Gateway logs: Include request routing and authentication
|
||||
|
||||
This microservices implementation provides a robust, scalable foundation for energy management systems with independent deployability, comprehensive monitoring, and production-ready features.
|
||||
97
microservices/README.md
Normal file
97
microservices/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Energy Management Microservices Architecture
|
||||
|
||||
This directory contains independent microservices based on the tiocps/iot-building-monitoring system, redesigned for modular deployment and scalability.
|
||||
|
||||
## Services Overview
|
||||
|
||||
### 1. **Token Service** (`token-service/`)
|
||||
- JWT token generation, validation, and management
|
||||
- Resource-based access control
|
||||
- Authentication service for all other services
|
||||
- **Port**: 8001
|
||||
|
||||
### 2. **Battery Management Service** (`battery-service/`)
|
||||
- Battery monitoring, charging, and discharging
|
||||
- Energy storage optimization
|
||||
- Battery health and state tracking
|
||||
- **Port**: 8002
|
||||
|
||||
### 3. **Demand Response Service** (`demand-response-service/`)
|
||||
- Grid interaction and demand response events
|
||||
- Load shifting coordination
|
||||
- Event scheduling and management
|
||||
- **Port**: 8003
|
||||
|
||||
### 4. **P2P Energy Trading Service** (`p2p-trading-service/`)
|
||||
- Peer-to-peer energy marketplace
|
||||
- Transaction management and pricing
|
||||
- Energy trading optimization
|
||||
- **Port**: 8004
|
||||
|
||||
### 5. **Forecasting Service** (`forecasting-service/`)
|
||||
- ML-based consumption and generation forecasting
|
||||
- Historical data analysis
|
||||
- Predictive analytics for optimization
|
||||
- **Port**: 8005
|
||||
|
||||
### 6. **IoT Control Service** (`iot-control-service/`)
|
||||
- IoT device management and control
|
||||
- Device instructions and automation
|
||||
- Real-time device monitoring
|
||||
- **Port**: 8006
|
||||
|
||||
### 7. **API Gateway** (`api-gateway/`)
|
||||
- Central entry point for all services
|
||||
- Request routing and load balancing
|
||||
- Authentication and rate limiting
|
||||
- **Port**: 8000
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
- **Independent Deployment**: Each service can be deployed, scaled, and updated independently
|
||||
- **Database per Service**: Each microservice has its own database/collection
|
||||
- **Event-Driven Communication**: Services communicate via Redis pub/sub for real-time events
|
||||
- **REST APIs**: Synchronous communication between services via REST
|
||||
- **Containerized**: Each service runs in its own Docker container
|
||||
|
||||
## Communication Patterns
|
||||
|
||||
1. **API Gateway → Services**: HTTP REST calls
|
||||
2. **Inter-Service Communication**: HTTP REST + Redis pub/sub for events
|
||||
3. **Real-time Updates**: Redis channels for WebSocket broadcasting
|
||||
4. **Data Persistence**: MongoDB with service-specific collections
|
||||
|
||||
## Deployment
|
||||
|
||||
Each service includes:
|
||||
- `main.py` - FastAPI application
|
||||
- `models.py` - Pydantic models
|
||||
- `database.py` - Database connection
|
||||
- `requirements.txt` - Dependencies
|
||||
- `Dockerfile` - Container configuration
|
||||
- `docker-compose.yml` - Service orchestration
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
docker-compose up -d
|
||||
|
||||
# Start individual service
|
||||
cd token-service && python main.py
|
||||
|
||||
# API Gateway (main entry point)
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
## Service Dependencies
|
||||
|
||||
```
|
||||
API Gateway (8000)
|
||||
├── Token Service (8001) - Authentication
|
||||
├── Battery Service (8002)
|
||||
├── Demand Response Service (8003)
|
||||
├── P2P Trading Service (8004)
|
||||
├── Forecasting Service (8005)
|
||||
└── IoT Control Service (8006)
|
||||
```
|
||||
26
microservices/api-gateway/Dockerfile
Normal file
26
microservices/api-gateway/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "main.py"]
|
||||
89
microservices/api-gateway/auth_middleware.py
Normal file
89
microservices/api-gateway/auth_middleware.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Authentication middleware for API Gateway
|
||||
"""
|
||||
|
||||
import aiohttp
|
||||
from fastapi import HTTPException, Request
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AuthMiddleware:
|
||||
"""Authentication middleware for validating tokens"""
|
||||
|
||||
def __init__(self, token_service_url: str = "http://localhost:8001"):
|
||||
self.token_service_url = token_service_url
|
||||
|
||||
async def verify_token(self, request: Request) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Verify authentication token from request headers
|
||||
Returns token payload if valid, raises HTTPException if invalid
|
||||
"""
|
||||
# Extract token from Authorization header
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header:
|
||||
raise HTTPException(status_code=401, detail="Authorization header required")
|
||||
|
||||
if not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Bearer token required")
|
||||
|
||||
token = auth_header[7:] # Remove "Bearer " prefix
|
||||
|
||||
try:
|
||||
# Validate token with token service
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{self.token_service_url}/tokens/validate",
|
||||
json={"token": token},
|
||||
timeout=aiohttp.ClientTimeout(total=5)
|
||||
) as response:
|
||||
|
||||
if response.status != 200:
|
||||
raise HTTPException(status_code=401, detail="Token validation failed")
|
||||
|
||||
token_data = await response.json()
|
||||
|
||||
if not token_data.get("valid"):
|
||||
error_msg = token_data.get("error", "Invalid token")
|
||||
raise HTTPException(status_code=401, detail=error_msg)
|
||||
|
||||
# Token is valid, return decoded payload
|
||||
return token_data.get("decoded")
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"Token service connection error: {e}")
|
||||
raise HTTPException(status_code=503, detail="Authentication service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Token verification error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Authentication error")
|
||||
|
||||
async def check_permissions(self, token_payload: Dict[str, Any], required_resources: list) -> bool:
|
||||
"""
|
||||
Check if token has required permissions for specific resources
|
||||
"""
|
||||
if not token_payload:
|
||||
return False
|
||||
|
||||
# Get list of resources the token has access to
|
||||
token_resources = token_payload.get("list_of_resources", [])
|
||||
|
||||
# Check if token has access to all required resources
|
||||
for resource in required_resources:
|
||||
if resource not in token_resources:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def extract_user_info(self, token_payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Extract user information from token payload"""
|
||||
return {
|
||||
"name": token_payload.get("name"),
|
||||
"resources": token_payload.get("list_of_resources", []),
|
||||
"data_aggregation": token_payload.get("data_aggregation", False),
|
||||
"time_aggregation": token_payload.get("time_aggregation", False),
|
||||
"embargo": token_payload.get("embargo", 0),
|
||||
"expires_at": token_payload.get("exp")
|
||||
}
|
||||
124
microservices/api-gateway/load_balancer.py
Normal file
124
microservices/api-gateway/load_balancer.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Load balancer for distributing requests across service instances
|
||||
"""
|
||||
|
||||
import random
|
||||
from typing import List, Dict, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class LoadBalancer:
|
||||
"""Simple load balancer for microservice requests"""
|
||||
|
||||
def __init__(self):
|
||||
# In a real implementation, this would track multiple instances per service
|
||||
self.service_instances: Dict[str, List[str]] = {}
|
||||
self.current_index: Dict[str, int] = {}
|
||||
|
||||
def register_service_instance(self, service_name: str, instance_url: str):
|
||||
"""Register a new service instance"""
|
||||
if service_name not in self.service_instances:
|
||||
self.service_instances[service_name] = []
|
||||
self.current_index[service_name] = 0
|
||||
|
||||
if instance_url not in self.service_instances[service_name]:
|
||||
self.service_instances[service_name].append(instance_url)
|
||||
logger.info(f"Registered instance {instance_url} for service {service_name}")
|
||||
|
||||
def unregister_service_instance(self, service_name: str, instance_url: str):
|
||||
"""Unregister a service instance"""
|
||||
if service_name in self.service_instances:
|
||||
try:
|
||||
self.service_instances[service_name].remove(instance_url)
|
||||
logger.info(f"Unregistered instance {instance_url} for service {service_name}")
|
||||
|
||||
# Reset index if it's out of bounds
|
||||
if self.current_index[service_name] >= len(self.service_instances[service_name]):
|
||||
self.current_index[service_name] = 0
|
||||
|
||||
except ValueError:
|
||||
logger.warning(f"Instance {instance_url} not found for service {service_name}")
|
||||
|
||||
async def get_service_url(self, service_name: str, strategy: str = "single") -> Optional[str]:
|
||||
"""
|
||||
Get a service URL using the specified load balancing strategy
|
||||
|
||||
Strategies:
|
||||
- single: Single instance (default for this simple implementation)
|
||||
- round_robin: Round-robin across instances
|
||||
- random: Random selection
|
||||
"""
|
||||
# For this microservice setup, we typically have one instance per service
|
||||
# In a production environment, you'd have multiple instances
|
||||
|
||||
if strategy == "single":
|
||||
# Default behavior - get the service URL from service registry
|
||||
from service_registry import ServiceRegistry
|
||||
service_registry = ServiceRegistry()
|
||||
return await service_registry.get_service_url(service_name)
|
||||
|
||||
elif strategy == "round_robin":
|
||||
return await self._round_robin_select(service_name)
|
||||
|
||||
elif strategy == "random":
|
||||
return await self._random_select(service_name)
|
||||
|
||||
else:
|
||||
logger.error(f"Unknown load balancing strategy: {strategy}")
|
||||
return None
|
||||
|
||||
async def _round_robin_select(self, service_name: str) -> Optional[str]:
|
||||
"""Select service instance using round-robin"""
|
||||
instances = self.service_instances.get(service_name, [])
|
||||
if not instances:
|
||||
# Fall back to service registry
|
||||
from service_registry import ServiceRegistry
|
||||
service_registry = ServiceRegistry()
|
||||
return await service_registry.get_service_url(service_name)
|
||||
|
||||
# Round-robin selection
|
||||
current_idx = self.current_index[service_name]
|
||||
selected_instance = instances[current_idx]
|
||||
|
||||
# Update index for next request
|
||||
self.current_index[service_name] = (current_idx + 1) % len(instances)
|
||||
|
||||
logger.debug(f"Round-robin selected {selected_instance} for {service_name}")
|
||||
return selected_instance
|
||||
|
||||
async def _random_select(self, service_name: str) -> Optional[str]:
|
||||
"""Select service instance randomly"""
|
||||
instances = self.service_instances.get(service_name, [])
|
||||
if not instances:
|
||||
# Fall back to service registry
|
||||
from service_registry import ServiceRegistry
|
||||
service_registry = ServiceRegistry()
|
||||
return await service_registry.get_service_url(service_name)
|
||||
|
||||
selected_instance = random.choice(instances)
|
||||
logger.debug(f"Random selected {selected_instance} for {service_name}")
|
||||
return selected_instance
|
||||
|
||||
def get_service_instances(self, service_name: str) -> List[str]:
|
||||
"""Get all registered instances for a service"""
|
||||
return self.service_instances.get(service_name, [])
|
||||
|
||||
def get_instance_count(self, service_name: str) -> int:
|
||||
"""Get number of registered instances for a service"""
|
||||
return len(self.service_instances.get(service_name, []))
|
||||
|
||||
def get_all_services(self) -> Dict[str, List[str]]:
|
||||
"""Get all services and their instances"""
|
||||
return self.service_instances.copy()
|
||||
|
||||
def health_check_failed(self, service_name: str, instance_url: str):
|
||||
"""Handle health check failure for a service instance"""
|
||||
logger.warning(f"Health check failed for {instance_url} ({service_name})")
|
||||
# In a production system, you might temporarily remove unhealthy instances
|
||||
# For now, we just log the failure
|
||||
|
||||
def health_check_recovered(self, service_name: str, instance_url: str):
|
||||
"""Handle health check recovery for a service instance"""
|
||||
logger.info(f"Health check recovered for {instance_url} ({service_name})")
|
||||
# Re-register the instance if it was temporarily removed
|
||||
352
microservices/api-gateway/main.py
Normal file
352
microservices/api-gateway/main.py
Normal file
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
API Gateway for Energy Management Microservices
|
||||
Central entry point that routes requests to appropriate microservices.
|
||||
Port: 8000
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
import os
|
||||
|
||||
from models import ServiceConfig, HealthResponse, GatewayStats
|
||||
from service_registry import ServiceRegistry
|
||||
from load_balancer import LoadBalancer
|
||||
from auth_middleware import AuthMiddleware
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager"""
|
||||
logger.info("API Gateway starting up...")
|
||||
|
||||
# Initialize service registry
|
||||
await service_registry.initialize()
|
||||
|
||||
# Start health check task
|
||||
asyncio.create_task(health_check_task())
|
||||
|
||||
logger.info("API Gateway startup complete")
|
||||
|
||||
yield
|
||||
|
||||
logger.info("API Gateway shutting down...")
|
||||
await service_registry.close()
|
||||
logger.info("API Gateway shutdown complete")
|
||||
|
||||
app = FastAPI(
|
||||
title="Energy Management API Gateway",
|
||||
description="Central API gateway for energy management microservices",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Service registry and load balancer
|
||||
service_registry = ServiceRegistry()
|
||||
load_balancer = LoadBalancer()
|
||||
auth_middleware = AuthMiddleware()
|
||||
|
||||
# Service configuration
|
||||
SERVICES = {
|
||||
"token-service": ServiceConfig(
|
||||
name="token-service",
|
||||
base_url="http://localhost:8001",
|
||||
health_endpoint="/health",
|
||||
auth_required=False
|
||||
),
|
||||
"battery-service": ServiceConfig(
|
||||
name="battery-service",
|
||||
base_url="http://localhost:8002",
|
||||
health_endpoint="/health",
|
||||
auth_required=True
|
||||
),
|
||||
"demand-response-service": ServiceConfig(
|
||||
name="demand-response-service",
|
||||
base_url="http://localhost:8003",
|
||||
health_endpoint="/health",
|
||||
auth_required=True
|
||||
),
|
||||
"p2p-trading-service": ServiceConfig(
|
||||
name="p2p-trading-service",
|
||||
base_url="http://localhost:8004",
|
||||
health_endpoint="/health",
|
||||
auth_required=True
|
||||
),
|
||||
"forecasting-service": ServiceConfig(
|
||||
name="forecasting-service",
|
||||
base_url="http://localhost:8005",
|
||||
health_endpoint="/health",
|
||||
auth_required=True
|
||||
),
|
||||
"iot-control-service": ServiceConfig(
|
||||
name="iot-control-service",
|
||||
base_url="http://localhost:8006",
|
||||
health_endpoint="/health",
|
||||
auth_required=True
|
||||
)
|
||||
}
|
||||
|
||||
# Request statistics
|
||||
request_stats = {
|
||||
"total_requests": 0,
|
||||
"successful_requests": 0,
|
||||
"failed_requests": 0,
|
||||
"service_requests": {service: 0 for service in SERVICES.keys()},
|
||||
"start_time": datetime.utcnow()
|
||||
}
|
||||
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def gateway_health_check():
|
||||
"""Gateway health check endpoint"""
|
||||
try:
|
||||
# Check all services
|
||||
service_health = await service_registry.get_all_service_health()
|
||||
|
||||
healthy_services = sum(1 for status in service_health.values() if status.get("status") == "healthy")
|
||||
total_services = len(SERVICES)
|
||||
|
||||
overall_status = "healthy" if healthy_services == total_services else "degraded"
|
||||
|
||||
return HealthResponse(
|
||||
service="api-gateway",
|
||||
status=overall_status,
|
||||
timestamp=datetime.utcnow(),
|
||||
version="1.0.0",
|
||||
services=service_health,
|
||||
healthy_services=healthy_services,
|
||||
total_services=total_services
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Gateway health check failed: {e}")
|
||||
raise HTTPException(status_code=503, detail="Service Unavailable")
|
||||
|
||||
@app.get("/services/status")
|
||||
async def get_services_status():
|
||||
"""Get status of all registered services"""
|
||||
try:
|
||||
service_health = await service_registry.get_all_service_health()
|
||||
return {
|
||||
"services": service_health,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"total_services": len(SERVICES),
|
||||
"healthy_services": sum(1 for status in service_health.values() if status.get("status") == "healthy")
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting services status: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/stats", response_model=GatewayStats)
|
||||
async def get_gateway_stats():
|
||||
"""Get API gateway statistics"""
|
||||
uptime = (datetime.utcnow() - request_stats["start_time"]).total_seconds()
|
||||
|
||||
return GatewayStats(
|
||||
total_requests=request_stats["total_requests"],
|
||||
successful_requests=request_stats["successful_requests"],
|
||||
failed_requests=request_stats["failed_requests"],
|
||||
success_rate=round((request_stats["successful_requests"] / max(request_stats["total_requests"], 1)) * 100, 2),
|
||||
uptime_seconds=uptime,
|
||||
service_requests=request_stats["service_requests"],
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
|
||||
# Token Service Routes
|
||||
@app.api_route("/api/v1/tokens/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def token_service_proxy(request: Request, path: str):
|
||||
"""Proxy requests to token service"""
|
||||
return await proxy_request(request, "token-service", f"/{path}")
|
||||
|
||||
# Battery Service Routes
|
||||
@app.api_route("/api/v1/batteries/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def battery_service_proxy(request: Request, path: str):
|
||||
"""Proxy requests to battery service"""
|
||||
return await proxy_request(request, "battery-service", f"/{path}")
|
||||
|
||||
# Demand Response Service Routes
|
||||
@app.api_route("/api/v1/demand-response/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def demand_response_service_proxy(request: Request, path: str):
|
||||
"""Proxy requests to demand response service"""
|
||||
return await proxy_request(request, "demand-response-service", f"/{path}")
|
||||
|
||||
# P2P Trading Service Routes
|
||||
@app.api_route("/api/v1/p2p/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def p2p_trading_service_proxy(request: Request, path: str):
|
||||
"""Proxy requests to P2P trading service"""
|
||||
return await proxy_request(request, "p2p-trading-service", f"/{path}")
|
||||
|
||||
# Forecasting Service Routes
|
||||
@app.api_route("/api/v1/forecast/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def forecasting_service_proxy(request: Request, path: str):
|
||||
"""Proxy requests to forecasting service"""
|
||||
return await proxy_request(request, "forecasting-service", f"/{path}")
|
||||
|
||||
# IoT Control Service Routes
|
||||
@app.api_route("/api/v1/iot/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def iot_control_service_proxy(request: Request, path: str):
|
||||
"""Proxy requests to IoT control service"""
|
||||
return await proxy_request(request, "iot-control-service", f"/{path}")
|
||||
|
||||
async def proxy_request(request: Request, service_name: str, path: str):
|
||||
"""Generic request proxy function"""
|
||||
try:
|
||||
# Update request statistics
|
||||
request_stats["total_requests"] += 1
|
||||
request_stats["service_requests"][service_name] += 1
|
||||
|
||||
# Get service configuration
|
||||
service_config = SERVICES.get(service_name)
|
||||
if not service_config:
|
||||
raise HTTPException(status_code=404, detail=f"Service {service_name} not found")
|
||||
|
||||
# Check authentication if required
|
||||
if service_config.auth_required:
|
||||
await auth_middleware.verify_token(request)
|
||||
|
||||
# Get healthy service instance
|
||||
service_url = await load_balancer.get_service_url(service_name)
|
||||
|
||||
# Prepare request
|
||||
url = f"{service_url}{path}"
|
||||
method = request.method
|
||||
headers = dict(request.headers)
|
||||
|
||||
# Remove hop-by-hop headers
|
||||
headers.pop("host", None)
|
||||
headers.pop("content-length", None)
|
||||
|
||||
# Get request body
|
||||
body = None
|
||||
if method in ["POST", "PUT", "PATCH"]:
|
||||
body = await request.body()
|
||||
|
||||
# Make request to service
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
data=body,
|
||||
params=dict(request.query_params),
|
||||
timeout=aiohttp.ClientTimeout(total=30)
|
||||
) as response:
|
||||
|
||||
# Get response data
|
||||
response_data = await response.read()
|
||||
response_headers = dict(response.headers)
|
||||
|
||||
# Remove hop-by-hop headers from response
|
||||
response_headers.pop("transfer-encoding", None)
|
||||
response_headers.pop("connection", None)
|
||||
|
||||
# Update success statistics
|
||||
if response.status < 400:
|
||||
request_stats["successful_requests"] += 1
|
||||
else:
|
||||
request_stats["failed_requests"] += 1
|
||||
|
||||
# Return response
|
||||
return Response(
|
||||
content=response_data,
|
||||
status_code=response.status,
|
||||
headers=response_headers,
|
||||
media_type=response_headers.get("content-type")
|
||||
)
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
request_stats["failed_requests"] += 1
|
||||
logger.error(f"Service {service_name} connection error: {e}")
|
||||
raise HTTPException(status_code=503, detail=f"Service {service_name} unavailable")
|
||||
|
||||
except HTTPException:
|
||||
request_stats["failed_requests"] += 1
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
request_stats["failed_requests"] += 1
|
||||
logger.error(f"Proxy error for {service_name}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal gateway error")
|
||||
|
||||
@app.get("/api/v1/overview")
|
||||
async def get_system_overview():
|
||||
"""Get comprehensive system overview from all services"""
|
||||
try:
|
||||
overview = {}
|
||||
|
||||
# Get data from each service
|
||||
for service_name in SERVICES.keys():
|
||||
try:
|
||||
if await service_registry.is_service_healthy(service_name):
|
||||
service_url = await load_balancer.get_service_url(service_name)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# Try to get service-specific overview data
|
||||
overview_endpoints = {
|
||||
"battery-service": "/batteries",
|
||||
"demand-response-service": "/flexibility/current",
|
||||
"p2p-trading-service": "/market/status",
|
||||
"forecasting-service": "/forecast/summary",
|
||||
"iot-control-service": "/devices/summary"
|
||||
}
|
||||
|
||||
endpoint = overview_endpoints.get(service_name)
|
||||
if endpoint:
|
||||
async with session.get(f"{service_url}{endpoint}", timeout=aiohttp.ClientTimeout(total=5)) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
overview[service_name] = data
|
||||
else:
|
||||
overview[service_name] = {"status": "error", "message": "Service returned error"}
|
||||
else:
|
||||
overview[service_name] = {"status": "available"}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get overview from {service_name}: {e}")
|
||||
overview[service_name] = {"status": "unavailable", "error": str(e)}
|
||||
|
||||
return {
|
||||
"system_overview": overview,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"services_checked": len(SERVICES)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting system overview: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def health_check_task():
|
||||
"""Background task for periodic health checks"""
|
||||
logger.info("Starting health check task")
|
||||
|
||||
while True:
|
||||
try:
|
||||
await service_registry.update_all_service_health()
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in health check task: {e}")
|
||||
await asyncio.sleep(60)
|
||||
|
||||
# Initialize service registry with services
|
||||
asyncio.create_task(service_registry.register_services(SERVICES))
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
77
microservices/api-gateway/models.py
Normal file
77
microservices/api-gateway/models.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Models for API Gateway
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
class ServiceConfig(BaseModel):
|
||||
"""Configuration for a microservice"""
|
||||
name: str
|
||||
base_url: str
|
||||
health_endpoint: str = "/health"
|
||||
auth_required: bool = True
|
||||
timeout_seconds: int = 30
|
||||
retry_attempts: int = 3
|
||||
|
||||
class ServiceHealth(BaseModel):
|
||||
"""Health status of a service"""
|
||||
service: str
|
||||
status: str # healthy, unhealthy, unknown
|
||||
response_time_ms: Optional[float] = None
|
||||
last_check: datetime
|
||||
error_message: Optional[str] = None
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Gateway health response"""
|
||||
service: str
|
||||
status: str
|
||||
timestamp: datetime
|
||||
version: str
|
||||
services: Optional[Dict[str, Any]] = None
|
||||
healthy_services: Optional[int] = None
|
||||
total_services: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
|
||||
class GatewayStats(BaseModel):
|
||||
"""API Gateway statistics"""
|
||||
total_requests: int
|
||||
successful_requests: int
|
||||
failed_requests: int
|
||||
success_rate: float
|
||||
uptime_seconds: float
|
||||
service_requests: Dict[str, int]
|
||||
timestamp: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
|
||||
class AuthToken(BaseModel):
|
||||
"""Authentication token model"""
|
||||
token: str
|
||||
user_id: Optional[str] = None
|
||||
permissions: List[str] = Field(default_factory=list)
|
||||
|
||||
class ProxyRequest(BaseModel):
|
||||
"""Proxy request model"""
|
||||
service: str
|
||||
path: str
|
||||
method: str
|
||||
headers: Dict[str, str]
|
||||
query_params: Dict[str, Any]
|
||||
body: Optional[bytes] = None
|
||||
|
||||
class ProxyResponse(BaseModel):
|
||||
"""Proxy response model"""
|
||||
status_code: int
|
||||
headers: Dict[str, str]
|
||||
body: bytes
|
||||
service: str
|
||||
response_time_ms: float
|
||||
5
microservices/api-gateway/requirements.txt
Normal file
5
microservices/api-gateway/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
aiohttp
|
||||
python-dotenv
|
||||
pydantic
|
||||
194
microservices/api-gateway/service_registry.py
Normal file
194
microservices/api-gateway/service_registry.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Service registry for managing microservice discovery and health monitoring
|
||||
"""
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
|
||||
from models import ServiceConfig, ServiceHealth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ServiceRegistry:
|
||||
"""Service registry for microservice management"""
|
||||
|
||||
def __init__(self):
|
||||
self.services: Dict[str, ServiceConfig] = {}
|
||||
self.service_health: Dict[str, ServiceHealth] = {}
|
||||
self.session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the service registry"""
|
||||
self.session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=10)
|
||||
)
|
||||
logger.info("Service registry initialized")
|
||||
|
||||
async def close(self):
|
||||
"""Close the service registry"""
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
logger.info("Service registry closed")
|
||||
|
||||
async def register_services(self, services: Dict[str, ServiceConfig]):
|
||||
"""Register multiple services"""
|
||||
self.services.update(services)
|
||||
|
||||
# Initialize health status for all services
|
||||
for service_name, config in services.items():
|
||||
self.service_health[service_name] = ServiceHealth(
|
||||
service=service_name,
|
||||
status="unknown",
|
||||
last_check=datetime.utcnow()
|
||||
)
|
||||
|
||||
logger.info(f"Registered {len(services)} services")
|
||||
|
||||
# Perform initial health check
|
||||
await self.update_all_service_health()
|
||||
|
||||
async def register_service(self, service_config: ServiceConfig):
|
||||
"""Register a single service"""
|
||||
self.services[service_config.name] = service_config
|
||||
self.service_health[service_config.name] = ServiceHealth(
|
||||
service=service_config.name,
|
||||
status="unknown",
|
||||
last_check=datetime.utcnow()
|
||||
)
|
||||
|
||||
logger.info(f"Registered service: {service_config.name}")
|
||||
|
||||
# Check health of the newly registered service
|
||||
await self.check_service_health(service_config.name)
|
||||
|
||||
async def unregister_service(self, service_name: str):
|
||||
"""Unregister a service"""
|
||||
self.services.pop(service_name, None)
|
||||
self.service_health.pop(service_name, None)
|
||||
logger.info(f"Unregistered service: {service_name}")
|
||||
|
||||
async def check_service_health(self, service_name: str) -> ServiceHealth:
|
||||
"""Check health of a specific service"""
|
||||
service_config = self.services.get(service_name)
|
||||
if not service_config:
|
||||
logger.error(f"Service {service_name} not found in registry")
|
||||
return ServiceHealth(
|
||||
service=service_name,
|
||||
status="unknown",
|
||||
last_check=datetime.utcnow(),
|
||||
error_message="Service not registered"
|
||||
)
|
||||
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
try:
|
||||
health_url = f"{service_config.base_url}{service_config.health_endpoint}"
|
||||
|
||||
async with self.session.get(health_url) as response:
|
||||
end_time = datetime.utcnow()
|
||||
response_time = (end_time - start_time).total_seconds() * 1000
|
||||
|
||||
if response.status == 200:
|
||||
health_data = await response.json()
|
||||
status = "healthy" if health_data.get("status") in ["healthy", "ok"] else "unhealthy"
|
||||
|
||||
health = ServiceHealth(
|
||||
service=service_name,
|
||||
status=status,
|
||||
response_time_ms=response_time,
|
||||
last_check=end_time
|
||||
)
|
||||
else:
|
||||
health = ServiceHealth(
|
||||
service=service_name,
|
||||
status="unhealthy",
|
||||
response_time_ms=response_time,
|
||||
last_check=end_time,
|
||||
error_message=f"HTTP {response.status}"
|
||||
)
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
health = ServiceHealth(
|
||||
service=service_name,
|
||||
status="unhealthy",
|
||||
last_check=datetime.utcnow(),
|
||||
error_message=f"Connection error: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
health = ServiceHealth(
|
||||
service=service_name,
|
||||
status="unhealthy",
|
||||
last_check=datetime.utcnow(),
|
||||
error_message=f"Health check failed: {str(e)}"
|
||||
)
|
||||
|
||||
# Update health status
|
||||
self.service_health[service_name] = health
|
||||
|
||||
# Log health status changes
|
||||
if health.status != "healthy":
|
||||
logger.warning(f"Service {service_name} health check failed: {health.error_message}")
|
||||
|
||||
return health
|
||||
|
||||
async def update_all_service_health(self):
|
||||
"""Update health status for all registered services"""
|
||||
health_checks = [
|
||||
self.check_service_health(service_name)
|
||||
for service_name in self.services.keys()
|
||||
]
|
||||
|
||||
if health_checks:
|
||||
await asyncio.gather(*health_checks, return_exceptions=True)
|
||||
|
||||
# Log summary
|
||||
healthy_count = sum(1 for h in self.service_health.values() if h.status == "healthy")
|
||||
total_count = len(self.services)
|
||||
logger.info(f"Health check complete: {healthy_count}/{total_count} services healthy")
|
||||
|
||||
async def get_service_health(self, service_name: str) -> Optional[ServiceHealth]:
|
||||
"""Get health status of a specific service"""
|
||||
return self.service_health.get(service_name)
|
||||
|
||||
async def get_all_service_health(self) -> Dict[str, Dict]:
|
||||
"""Get health status of all services"""
|
||||
health_dict = {}
|
||||
for service_name, health in self.service_health.items():
|
||||
health_dict[service_name] = {
|
||||
"status": health.status,
|
||||
"response_time_ms": health.response_time_ms,
|
||||
"last_check": health.last_check.isoformat(),
|
||||
"error_message": health.error_message
|
||||
}
|
||||
return health_dict
|
||||
|
||||
async def is_service_healthy(self, service_name: str) -> bool:
|
||||
"""Check if a service is healthy"""
|
||||
health = self.service_health.get(service_name)
|
||||
return health is not None and health.status == "healthy"
|
||||
|
||||
async def get_healthy_services(self) -> List[str]:
|
||||
"""Get list of healthy service names"""
|
||||
return [
|
||||
service_name
|
||||
for service_name, health in self.service_health.items()
|
||||
if health.status == "healthy"
|
||||
]
|
||||
|
||||
def get_service_config(self, service_name: str) -> Optional[ServiceConfig]:
|
||||
"""Get configuration for a specific service"""
|
||||
return self.services.get(service_name)
|
||||
|
||||
def get_all_services(self) -> Dict[str, ServiceConfig]:
|
||||
"""Get all registered services"""
|
||||
return self.services.copy()
|
||||
|
||||
async def get_service_url(self, service_name: str) -> Optional[str]:
|
||||
"""Get base URL for a healthy service"""
|
||||
if await self.is_service_healthy(service_name):
|
||||
service_config = self.services.get(service_name)
|
||||
return service_config.base_url if service_config else None
|
||||
return None
|
||||
26
microservices/battery-service/Dockerfile
Normal file
26
microservices/battery-service/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8002
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8002/health || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "main.py"]
|
||||
414
microservices/battery-service/battery_service.py
Normal file
414
microservices/battery-service/battery_service.py
Normal file
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
Battery management service implementation
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
from motor.motor_asyncio import AsyncIOMotorDatabase
|
||||
import redis.asyncio as redis
|
||||
import logging
|
||||
import json
|
||||
|
||||
from models import BatteryState, BatteryType, MaintenanceAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BatteryService:
|
||||
"""Service for managing battery operations and monitoring"""
|
||||
|
||||
def __init__(self, db: AsyncIOMotorDatabase, redis_client: redis.Redis):
|
||||
self.db = db
|
||||
self.redis = redis_client
|
||||
self.batteries_collection = db.batteries
|
||||
self.battery_history_collection = db.battery_history
|
||||
self.maintenance_alerts_collection = db.maintenance_alerts
|
||||
|
||||
async def get_batteries(self) -> List[Dict[str, Any]]:
|
||||
"""Get all registered batteries"""
|
||||
cursor = self.batteries_collection.find({})
|
||||
batteries = []
|
||||
|
||||
async for battery in cursor:
|
||||
battery["_id"] = str(battery["_id"])
|
||||
# Convert datetime fields to ISO format
|
||||
for field in ["installed_date", "last_maintenance", "next_maintenance", "last_updated"]:
|
||||
if field in battery and battery[field]:
|
||||
battery[field] = battery[field].isoformat()
|
||||
|
||||
batteries.append(battery)
|
||||
|
||||
return batteries
|
||||
|
||||
async def get_battery_status(self, battery_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get current status of a specific battery"""
|
||||
# First try to get from Redis cache
|
||||
cached_status = await self.redis.get(f"battery:status:{battery_id}")
|
||||
if cached_status:
|
||||
return json.loads(cached_status)
|
||||
|
||||
# Fall back to database
|
||||
battery = await self.batteries_collection.find_one({"battery_id": battery_id})
|
||||
if battery:
|
||||
battery["_id"] = str(battery["_id"])
|
||||
|
||||
# Convert datetime fields
|
||||
for field in ["installed_date", "last_maintenance", "next_maintenance", "last_updated"]:
|
||||
if field in battery and battery[field]:
|
||||
battery[field] = battery[field].isoformat()
|
||||
|
||||
# Cache the result
|
||||
await self.redis.setex(
|
||||
f"battery:status:{battery_id}",
|
||||
300, # 5 minutes TTL
|
||||
json.dumps(battery, default=str)
|
||||
)
|
||||
|
||||
return battery
|
||||
|
||||
return None
|
||||
|
||||
async def charge_battery(self, battery_id: str, power_kw: float, duration_minutes: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""Initiate battery charging"""
|
||||
battery = await self.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
return {"success": False, "error": "Battery not found"}
|
||||
|
||||
# Check if battery can accept charge
|
||||
current_soc = battery.get("state_of_charge", 0)
|
||||
max_charge_power = battery.get("max_charge_power_kw", 0)
|
||||
|
||||
if current_soc >= 100:
|
||||
return {"success": False, "error": "Battery is already fully charged"}
|
||||
|
||||
if power_kw > max_charge_power:
|
||||
return {"success": False, "error": f"Requested power ({power_kw} kW) exceeds maximum charge power ({max_charge_power} kW)"}
|
||||
|
||||
# Update battery state
|
||||
now = datetime.utcnow()
|
||||
update_data = {
|
||||
"state": BatteryState.CHARGING.value,
|
||||
"current_power_kw": power_kw,
|
||||
"last_updated": now
|
||||
}
|
||||
|
||||
if duration_minutes:
|
||||
update_data["charging_until"] = now + timedelta(minutes=duration_minutes)
|
||||
|
||||
await self.batteries_collection.update_one(
|
||||
{"battery_id": battery_id},
|
||||
{"$set": update_data}
|
||||
)
|
||||
|
||||
# Clear cache
|
||||
await self.redis.delete(f"battery:status:{battery_id}")
|
||||
|
||||
# Log the charging event
|
||||
await self._log_battery_event(battery_id, "charging_started", {
|
||||
"power_kw": power_kw,
|
||||
"duration_minutes": duration_minutes
|
||||
})
|
||||
|
||||
# Publish event to Redis for real-time updates
|
||||
await self.redis.publish("battery_events", json.dumps({
|
||||
"event": "charging_started",
|
||||
"battery_id": battery_id,
|
||||
"power_kw": power_kw,
|
||||
"timestamp": now.isoformat()
|
||||
}))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"estimated_completion": (now + timedelta(minutes=duration_minutes)).isoformat() if duration_minutes else None
|
||||
}
|
||||
|
||||
async def discharge_battery(self, battery_id: str, power_kw: float, duration_minutes: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""Initiate battery discharging"""
|
||||
battery = await self.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
return {"success": False, "error": "Battery not found"}
|
||||
|
||||
# Check if battery can discharge
|
||||
current_soc = battery.get("state_of_charge", 0)
|
||||
max_discharge_power = battery.get("max_discharge_power_kw", 0)
|
||||
|
||||
if current_soc <= 0:
|
||||
return {"success": False, "error": "Battery is already empty"}
|
||||
|
||||
if power_kw > max_discharge_power:
|
||||
return {"success": False, "error": f"Requested power ({power_kw} kW) exceeds maximum discharge power ({max_discharge_power} kW)"}
|
||||
|
||||
# Update battery state
|
||||
now = datetime.utcnow()
|
||||
update_data = {
|
||||
"state": BatteryState.DISCHARGING.value,
|
||||
"current_power_kw": -power_kw, # Negative for discharging
|
||||
"last_updated": now
|
||||
}
|
||||
|
||||
if duration_minutes:
|
||||
update_data["discharging_until"] = now + timedelta(minutes=duration_minutes)
|
||||
|
||||
await self.batteries_collection.update_one(
|
||||
{"battery_id": battery_id},
|
||||
{"$set": update_data}
|
||||
)
|
||||
|
||||
# Clear cache
|
||||
await self.redis.delete(f"battery:status:{battery_id}")
|
||||
|
||||
# Log the discharging event
|
||||
await self._log_battery_event(battery_id, "discharging_started", {
|
||||
"power_kw": power_kw,
|
||||
"duration_minutes": duration_minutes
|
||||
})
|
||||
|
||||
# Publish event
|
||||
await self.redis.publish("battery_events", json.dumps({
|
||||
"event": "discharging_started",
|
||||
"battery_id": battery_id,
|
||||
"power_kw": power_kw,
|
||||
"timestamp": now.isoformat()
|
||||
}))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"estimated_completion": (now + timedelta(minutes=duration_minutes)).isoformat() if duration_minutes else None
|
||||
}
|
||||
|
||||
async def optimize_battery(self, battery_id: str, target_soc: float) -> Dict[str, Any]:
|
||||
"""Optimize battery charging/discharging to reach target SOC"""
|
||||
battery = await self.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
return {"success": False, "error": "Battery not found"}
|
||||
|
||||
current_soc = battery.get("state_of_charge", 0)
|
||||
capacity_kwh = battery.get("capacity_kwh", 0)
|
||||
|
||||
# Calculate energy needed
|
||||
energy_difference_kwh = (target_soc - current_soc) / 100 * capacity_kwh
|
||||
|
||||
if abs(energy_difference_kwh) < 0.1: # Within 0.1 kWh
|
||||
return {"message": "Battery is already at target SOC", "action": "none"}
|
||||
|
||||
if energy_difference_kwh > 0:
|
||||
# Need to charge
|
||||
max_power = battery.get("max_charge_power_kw", 0)
|
||||
action = "charge"
|
||||
else:
|
||||
# Need to discharge
|
||||
max_power = battery.get("max_discharge_power_kw", 0)
|
||||
action = "discharge"
|
||||
energy_difference_kwh = abs(energy_difference_kwh)
|
||||
|
||||
# Calculate optimal power and duration
|
||||
optimal_power = min(max_power, energy_difference_kwh * 2) # Conservative power level
|
||||
duration_hours = energy_difference_kwh / optimal_power
|
||||
duration_minutes = int(duration_hours * 60)
|
||||
|
||||
# Execute the optimization
|
||||
if action == "charge":
|
||||
result = await self.charge_battery(battery_id, optimal_power, duration_minutes)
|
||||
else:
|
||||
result = await self.discharge_battery(battery_id, optimal_power, duration_minutes)
|
||||
|
||||
return {
|
||||
"action": action,
|
||||
"power_kw": optimal_power,
|
||||
"duration_minutes": duration_minutes,
|
||||
"energy_difference_kwh": energy_difference_kwh,
|
||||
"result": result
|
||||
}
|
||||
|
||||
async def get_battery_history(self, battery_id: str, hours: int = 24) -> List[Dict[str, Any]]:
|
||||
"""Get historical data for a battery"""
|
||||
start_time = datetime.utcnow() - timedelta(hours=hours)
|
||||
|
||||
cursor = self.battery_history_collection.find({
|
||||
"battery_id": battery_id,
|
||||
"timestamp": {"$gte": start_time}
|
||||
}).sort("timestamp", -1)
|
||||
|
||||
history = []
|
||||
async for record in cursor:
|
||||
record["_id"] = str(record["_id"])
|
||||
if "timestamp" in record:
|
||||
record["timestamp"] = record["timestamp"].isoformat()
|
||||
history.append(record)
|
||||
|
||||
return history
|
||||
|
||||
async def get_battery_analytics(self, hours: int = 24) -> Dict[str, Any]:
|
||||
"""Get system-wide battery analytics"""
|
||||
start_time = datetime.utcnow() - timedelta(hours=hours)
|
||||
|
||||
# Get all batteries
|
||||
batteries = await self.get_batteries()
|
||||
|
||||
total_capacity = sum(b.get("capacity_kwh", 0) for b in batteries)
|
||||
total_stored = sum(b.get("stored_energy_kwh", 0) for b in batteries)
|
||||
active_count = sum(1 for b in batteries if b.get("state") != "error")
|
||||
|
||||
# Aggregate historical data
|
||||
pipeline = [
|
||||
{"$match": {"timestamp": {"$gte": start_time}}},
|
||||
{"$group": {
|
||||
"_id": None,
|
||||
"total_energy_charged": {"$sum": {"$cond": [{"$gt": ["$power_kw", 0]}, {"$multiply": ["$power_kw", 0.5]}, 0]}}, # Approximate kWh
|
||||
"total_energy_discharged": {"$sum": {"$cond": [{"$lt": ["$power_kw", 0]}, {"$multiply": [{"$abs": "$power_kw"}, 0.5]}, 0]}},
|
||||
"avg_efficiency": {"$avg": "$efficiency"}
|
||||
}}
|
||||
]
|
||||
|
||||
cursor = self.battery_history_collection.aggregate(pipeline)
|
||||
analytics_data = await cursor.to_list(length=1)
|
||||
|
||||
if analytics_data:
|
||||
energy_data = analytics_data[0]
|
||||
else:
|
||||
energy_data = {
|
||||
"total_energy_charged": 0,
|
||||
"total_energy_discharged": 0,
|
||||
"avg_efficiency": 0.95
|
||||
}
|
||||
|
||||
# Calculate metrics
|
||||
average_soc = sum(b.get("state_of_charge", 0) for b in batteries) / len(batteries) if batteries else 0
|
||||
average_health = sum(b.get("health_percentage", 100) for b in batteries) / len(batteries) if batteries else 100
|
||||
|
||||
return {
|
||||
"total_batteries": len(batteries),
|
||||
"active_batteries": active_count,
|
||||
"total_capacity_kwh": total_capacity,
|
||||
"total_stored_energy_kwh": total_stored,
|
||||
"average_soc": round(average_soc, 2),
|
||||
"total_energy_charged_kwh": round(energy_data["total_energy_charged"], 2),
|
||||
"total_energy_discharged_kwh": round(energy_data["total_energy_discharged"], 2),
|
||||
"net_energy_flow_kwh": round(energy_data["total_energy_charged"] - energy_data["total_energy_discharged"], 2),
|
||||
"round_trip_efficiency": round(energy_data.get("avg_efficiency", 0.95) * 100, 2),
|
||||
"capacity_utilization": round((total_stored / total_capacity * 100) if total_capacity > 0 else 0, 2),
|
||||
"average_health": round(average_health, 2),
|
||||
"batteries_needing_maintenance": sum(1 for b in batteries if b.get("health_percentage", 100) < 80)
|
||||
}
|
||||
|
||||
async def update_battery_status(self, battery_id: str):
|
||||
"""Update battery status with simulated or real data"""
|
||||
# This would typically connect to actual battery management systems
|
||||
# For now, we'll simulate some basic updates
|
||||
|
||||
battery = await self.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
return
|
||||
|
||||
now = datetime.utcnow()
|
||||
current_power = battery.get("current_power_kw", 0)
|
||||
current_soc = battery.get("state_of_charge", 50)
|
||||
capacity = battery.get("capacity_kwh", 100)
|
||||
|
||||
# Simulate SOC changes based on power flow
|
||||
if current_power != 0:
|
||||
# Convert power to SOC change (simplified)
|
||||
soc_change = (current_power * 0.5) / capacity * 100 # 0.5 hour interval
|
||||
new_soc = max(0, min(100, current_soc + soc_change))
|
||||
|
||||
# Calculate stored energy
|
||||
stored_energy = new_soc / 100 * capacity
|
||||
|
||||
# Update database
|
||||
await self.batteries_collection.update_one(
|
||||
{"battery_id": battery_id},
|
||||
{
|
||||
"$set": {
|
||||
"state_of_charge": round(new_soc, 2),
|
||||
"stored_energy_kwh": round(stored_energy, 2),
|
||||
"last_updated": now
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Log historical data
|
||||
await self.battery_history_collection.insert_one({
|
||||
"battery_id": battery_id,
|
||||
"timestamp": now,
|
||||
"state_of_charge": new_soc,
|
||||
"power_kw": current_power,
|
||||
"stored_energy_kwh": stored_energy,
|
||||
"efficiency": battery.get("efficiency", 0.95)
|
||||
})
|
||||
|
||||
# Clear cache
|
||||
await self.redis.delete(f"battery:status:{battery_id}")
|
||||
|
||||
async def check_maintenance_alerts(self):
|
||||
"""Check for batteries needing maintenance"""
|
||||
batteries = await self.get_batteries()
|
||||
|
||||
for battery in batteries:
|
||||
alerts = []
|
||||
|
||||
# Check health
|
||||
health = battery.get("health_percentage", 100)
|
||||
if health < 70:
|
||||
alerts.append({
|
||||
"alert_type": "health",
|
||||
"severity": "critical",
|
||||
"message": f"Battery health is critically low at {health}%",
|
||||
"recommended_action": "Schedule immediate maintenance and consider replacement"
|
||||
})
|
||||
elif health < 85:
|
||||
alerts.append({
|
||||
"alert_type": "health",
|
||||
"severity": "warning",
|
||||
"message": f"Battery health is declining at {health}%",
|
||||
"recommended_action": "Schedule maintenance inspection"
|
||||
})
|
||||
|
||||
# Check cycles
|
||||
cycles = battery.get("cycles_completed", 0)
|
||||
max_cycles = battery.get("max_cycles", 5000)
|
||||
if cycles > max_cycles * 0.9:
|
||||
alerts.append({
|
||||
"alert_type": "cycles",
|
||||
"severity": "warning",
|
||||
"message": f"Battery has completed {cycles}/{max_cycles} cycles",
|
||||
"recommended_action": "Plan for battery replacement"
|
||||
})
|
||||
|
||||
# Check scheduled maintenance
|
||||
next_maintenance = battery.get("next_maintenance")
|
||||
if next_maintenance and datetime.fromisoformat(next_maintenance.replace('Z', '+00:00')) < datetime.utcnow():
|
||||
alerts.append({
|
||||
"alert_type": "scheduled",
|
||||
"severity": "info",
|
||||
"message": "Scheduled maintenance is due",
|
||||
"recommended_action": "Perform scheduled maintenance procedures"
|
||||
})
|
||||
|
||||
# Save alerts to database
|
||||
for alert in alerts:
|
||||
alert_doc = {
|
||||
"battery_id": battery["battery_id"],
|
||||
"timestamp": datetime.utcnow(),
|
||||
**alert
|
||||
}
|
||||
|
||||
# Check if alert already exists to avoid duplicates
|
||||
existing = await self.maintenance_alerts_collection.find_one({
|
||||
"battery_id": battery["battery_id"],
|
||||
"alert_type": alert["alert_type"],
|
||||
"severity": alert["severity"]
|
||||
})
|
||||
|
||||
if not existing:
|
||||
await self.maintenance_alerts_collection.insert_one(alert_doc)
|
||||
|
||||
async def _log_battery_event(self, battery_id: str, event_type: str, data: Dict[str, Any]):
|
||||
"""Log battery events for auditing"""
|
||||
event_doc = {
|
||||
"battery_id": battery_id,
|
||||
"event_type": event_type,
|
||||
"timestamp": datetime.utcnow(),
|
||||
"data": data
|
||||
}
|
||||
|
||||
await self.db.battery_events.insert_one(event_doc)
|
||||
104
microservices/battery-service/database.py
Normal file
104
microservices/battery-service/database.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Database connections for Battery Service
|
||||
"""
|
||||
|
||||
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
||||
import redis.asyncio as redis
|
||||
import logging
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Database configuration
|
||||
MONGO_URL = os.getenv("MONGO_URL", "mongodb://localhost:27017")
|
||||
DATABASE_NAME = os.getenv("DATABASE_NAME", "energy_dashboard_batteries")
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
|
||||
|
||||
# Global database clients
|
||||
_mongo_client: AsyncIOMotorClient = None
|
||||
_database: AsyncIOMotorDatabase = None
|
||||
_redis_client: redis.Redis = None
|
||||
|
||||
async def connect_to_mongo():
|
||||
"""Create MongoDB connection"""
|
||||
global _mongo_client, _database
|
||||
|
||||
try:
|
||||
_mongo_client = AsyncIOMotorClient(MONGO_URL)
|
||||
_database = _mongo_client[DATABASE_NAME]
|
||||
|
||||
# Test connection
|
||||
await _database.command("ping")
|
||||
logger.info(f"Connected to MongoDB: {DATABASE_NAME}")
|
||||
|
||||
# Create indexes
|
||||
await create_indexes()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to MongoDB: {e}")
|
||||
raise
|
||||
|
||||
async def connect_to_redis():
|
||||
"""Create Redis connection"""
|
||||
global _redis_client
|
||||
|
||||
try:
|
||||
_redis_client = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
await _redis_client.ping()
|
||||
logger.info("Connected to Redis")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Redis: {e}")
|
||||
raise
|
||||
|
||||
async def close_mongo_connection():
|
||||
"""Close MongoDB connection"""
|
||||
global _mongo_client
|
||||
|
||||
if _mongo_client:
|
||||
_mongo_client.close()
|
||||
logger.info("Disconnected from MongoDB")
|
||||
|
||||
async def get_database() -> AsyncIOMotorDatabase:
|
||||
"""Get database instance"""
|
||||
global _database
|
||||
|
||||
if _database is None:
|
||||
raise RuntimeError("Database not initialized. Call connect_to_mongo() first.")
|
||||
|
||||
return _database
|
||||
|
||||
async def get_redis() -> redis.Redis:
|
||||
"""Get Redis instance"""
|
||||
global _redis_client
|
||||
|
||||
if _redis_client is None:
|
||||
raise RuntimeError("Redis not initialized. Call connect_to_redis() first.")
|
||||
|
||||
return _redis_client
|
||||
|
||||
async def create_indexes():
|
||||
"""Create database indexes for performance"""
|
||||
db = await get_database()
|
||||
|
||||
# Indexes for batteries collection
|
||||
await db.batteries.create_index("battery_id", unique=True)
|
||||
await db.batteries.create_index("state")
|
||||
await db.batteries.create_index("building")
|
||||
await db.batteries.create_index("room")
|
||||
await db.batteries.create_index("last_updated")
|
||||
|
||||
# Indexes for battery_history collection
|
||||
await db.battery_history.create_index([("battery_id", 1), ("timestamp", -1)])
|
||||
await db.battery_history.create_index("timestamp")
|
||||
|
||||
# Indexes for maintenance_alerts collection
|
||||
await db.maintenance_alerts.create_index([("battery_id", 1), ("alert_type", 1)])
|
||||
await db.maintenance_alerts.create_index("timestamp")
|
||||
await db.maintenance_alerts.create_index("severity")
|
||||
|
||||
# Indexes for battery_events collection
|
||||
await db.battery_events.create_index([("battery_id", 1), ("timestamp", -1)])
|
||||
await db.battery_events.create_index("event_type")
|
||||
|
||||
logger.info("Database indexes created")
|
||||
262
microservices/battery-service/main.py
Normal file
262
microservices/battery-service/main.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Battery Management Microservice
|
||||
Handles battery monitoring, charging, and energy storage optimization.
|
||||
Port: 8002
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from models import (
|
||||
BatteryStatus, BatteryCommand, BatteryResponse, BatteryListResponse,
|
||||
ChargingRequest, HistoricalDataRequest, HealthResponse
|
||||
)
|
||||
from database import connect_to_mongo, close_mongo_connection, get_database, connect_to_redis, get_redis
|
||||
from battery_service import BatteryService
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager"""
|
||||
logger.info("Battery Service starting up...")
|
||||
await connect_to_mongo()
|
||||
await connect_to_redis()
|
||||
|
||||
# Start background tasks
|
||||
asyncio.create_task(battery_monitoring_task())
|
||||
|
||||
logger.info("Battery Service startup complete")
|
||||
|
||||
yield
|
||||
|
||||
logger.info("Battery Service shutting down...")
|
||||
await close_mongo_connection()
|
||||
logger.info("Battery Service shutdown complete")
|
||||
|
||||
app = FastAPI(
|
||||
title="Battery Management Service",
|
||||
description="Energy storage monitoring and control microservice",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Dependencies
|
||||
async def get_db():
|
||||
return await get_database()
|
||||
|
||||
async def get_battery_service(db=Depends(get_db)):
|
||||
redis = await get_redis()
|
||||
return BatteryService(db, redis)
|
||||
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
try:
|
||||
db = await get_database()
|
||||
await db.command("ping")
|
||||
|
||||
redis = await get_redis()
|
||||
await redis.ping()
|
||||
|
||||
return HealthResponse(
|
||||
service="battery-service",
|
||||
status="healthy",
|
||||
timestamp=datetime.utcnow(),
|
||||
version="1.0.0"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
raise HTTPException(status_code=503, detail="Service Unavailable")
|
||||
|
||||
@app.get("/batteries", response_model=BatteryListResponse)
|
||||
async def get_batteries(service: BatteryService = Depends(get_battery_service)):
|
||||
"""Get all registered batteries"""
|
||||
try:
|
||||
batteries = await service.get_batteries()
|
||||
return BatteryListResponse(
|
||||
batteries=batteries,
|
||||
count=len(batteries),
|
||||
total_capacity=sum(b.get("capacity", 0) for b in batteries),
|
||||
total_stored_energy=sum(b.get("stored_energy", 0) for b in batteries)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting batteries: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/batteries/{battery_id}", response_model=BatteryResponse)
|
||||
async def get_battery(battery_id: str, service: BatteryService = Depends(get_battery_service)):
|
||||
"""Get specific battery status"""
|
||||
try:
|
||||
battery = await service.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
raise HTTPException(status_code=404, detail="Battery not found")
|
||||
|
||||
return BatteryResponse(
|
||||
battery_id=battery_id,
|
||||
status=battery
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting battery {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/batteries/{battery_id}/charge")
|
||||
async def charge_battery(
|
||||
battery_id: str,
|
||||
request: ChargingRequest,
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Charge a battery with specified power"""
|
||||
try:
|
||||
result = await service.charge_battery(battery_id, request.power_kw, request.duration_minutes)
|
||||
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=400, detail=result.get("error", "Charging failed"))
|
||||
|
||||
return {
|
||||
"message": "Charging initiated successfully",
|
||||
"battery_id": battery_id,
|
||||
"power_kw": request.power_kw,
|
||||
"duration_minutes": request.duration_minutes,
|
||||
"estimated_completion": result.get("estimated_completion")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error charging battery {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/batteries/{battery_id}/discharge")
|
||||
async def discharge_battery(
|
||||
battery_id: str,
|
||||
request: ChargingRequest,
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Discharge a battery with specified power"""
|
||||
try:
|
||||
result = await service.discharge_battery(battery_id, request.power_kw, request.duration_minutes)
|
||||
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=400, detail=result.get("error", "Discharging failed"))
|
||||
|
||||
return {
|
||||
"message": "Discharging initiated successfully",
|
||||
"battery_id": battery_id,
|
||||
"power_kw": request.power_kw,
|
||||
"duration_minutes": request.duration_minutes,
|
||||
"estimated_completion": result.get("estimated_completion")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error discharging battery {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/batteries/{battery_id}/history")
|
||||
async def get_battery_history(
|
||||
battery_id: str,
|
||||
hours: int = 24,
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Get battery historical data"""
|
||||
try:
|
||||
history = await service.get_battery_history(battery_id, hours)
|
||||
|
||||
return {
|
||||
"battery_id": battery_id,
|
||||
"period_hours": hours,
|
||||
"history": history,
|
||||
"data_points": len(history)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting battery history for {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/batteries/analytics/summary")
|
||||
async def get_battery_analytics(
|
||||
hours: int = 24,
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Get battery system analytics"""
|
||||
try:
|
||||
analytics = await service.get_battery_analytics(hours)
|
||||
|
||||
return {
|
||||
"period_hours": hours,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
**analytics
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting battery analytics: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/batteries/{battery_id}/optimize")
|
||||
async def optimize_battery(
|
||||
battery_id: str,
|
||||
target_soc: float, # State of Charge target (0-100%)
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Optimize battery charging/discharging to reach target SOC"""
|
||||
try:
|
||||
if not (0 <= target_soc <= 100):
|
||||
raise HTTPException(status_code=400, detail="Target SOC must be between 0 and 100")
|
||||
|
||||
result = await service.optimize_battery(battery_id, target_soc)
|
||||
|
||||
return {
|
||||
"battery_id": battery_id,
|
||||
"target_soc": target_soc,
|
||||
"optimization_plan": result,
|
||||
"message": "Battery optimization initiated"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing battery {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def battery_monitoring_task():
|
||||
"""Background task for continuous battery monitoring"""
|
||||
logger.info("Starting battery monitoring task")
|
||||
|
||||
while True:
|
||||
try:
|
||||
db = await get_database()
|
||||
redis = await get_redis()
|
||||
service = BatteryService(db, redis)
|
||||
|
||||
# Update all battery statuses
|
||||
batteries = await service.get_batteries()
|
||||
for battery in batteries:
|
||||
await service.update_battery_status(battery["battery_id"])
|
||||
|
||||
# Check for maintenance alerts
|
||||
await service.check_maintenance_alerts()
|
||||
|
||||
# Sleep for monitoring interval (30 seconds)
|
||||
await asyncio.sleep(30)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in battery monitoring task: {e}")
|
||||
await asyncio.sleep(60) # Wait longer on error
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||
157
microservices/battery-service/models.py
Normal file
157
microservices/battery-service/models.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Pydantic models for Battery Management Service
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any, Literal
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
class BatteryState(str, Enum):
|
||||
IDLE = "idle"
|
||||
CHARGING = "charging"
|
||||
DISCHARGING = "discharging"
|
||||
MAINTENANCE = "maintenance"
|
||||
ERROR = "error"
|
||||
|
||||
class BatteryType(str, Enum):
|
||||
LITHIUM_ION = "lithium_ion"
|
||||
LEAD_ACID = "lead_acid"
|
||||
NICKEL_METAL_HYDRIDE = "nickel_metal_hydride"
|
||||
FLOW_BATTERY = "flow_battery"
|
||||
|
||||
class BatteryStatus(BaseModel):
|
||||
"""Battery status model"""
|
||||
battery_id: str = Field(..., description="Unique battery identifier")
|
||||
name: str = Field(..., description="Human-readable battery name")
|
||||
type: BatteryType = Field(..., description="Battery technology type")
|
||||
state: BatteryState = Field(..., description="Current operational state")
|
||||
|
||||
# Energy metrics
|
||||
capacity_kwh: float = Field(..., description="Total battery capacity in kWh")
|
||||
stored_energy_kwh: float = Field(..., description="Currently stored energy in kWh")
|
||||
state_of_charge: float = Field(..., description="State of charge (0-100%)")
|
||||
|
||||
# Power metrics
|
||||
max_charge_power_kw: float = Field(..., description="Maximum charging power in kW")
|
||||
max_discharge_power_kw: float = Field(..., description="Maximum discharging power in kW")
|
||||
current_power_kw: float = Field(0, description="Current power flow in kW (positive = charging)")
|
||||
|
||||
# Technical specifications
|
||||
efficiency: float = Field(0.95, description="Round-trip efficiency (0-1)")
|
||||
cycles_completed: int = Field(0, description="Number of charge/discharge cycles")
|
||||
max_cycles: int = Field(5000, description="Maximum rated cycles")
|
||||
|
||||
# Health and maintenance
|
||||
health_percentage: float = Field(100, description="Battery health (0-100%)")
|
||||
temperature_celsius: Optional[float] = Field(None, description="Battery temperature")
|
||||
last_maintenance: Optional[datetime] = Field(None, description="Last maintenance date")
|
||||
next_maintenance: Optional[datetime] = Field(None, description="Next maintenance date")
|
||||
|
||||
# Location and installation
|
||||
location: Optional[str] = Field(None, description="Physical location")
|
||||
building: Optional[str] = Field(None, description="Building identifier")
|
||||
room: Optional[str] = Field(None, description="Room identifier")
|
||||
|
||||
# Operational data
|
||||
installed_date: Optional[datetime] = Field(None, description="Installation date")
|
||||
last_updated: datetime = Field(default_factory=datetime.utcnow, description="Last status update")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None
|
||||
}
|
||||
|
||||
class BatteryCommand(BaseModel):
|
||||
"""Battery control command"""
|
||||
battery_id: str = Field(..., description="Target battery ID")
|
||||
command: Literal["charge", "discharge", "stop"] = Field(..., description="Command type")
|
||||
power_kw: Optional[float] = Field(None, description="Power level in kW")
|
||||
duration_minutes: Optional[int] = Field(None, description="Command duration in minutes")
|
||||
target_soc: Optional[float] = Field(None, description="Target state of charge (0-100%)")
|
||||
|
||||
class ChargingRequest(BaseModel):
|
||||
"""Battery charging/discharging request"""
|
||||
power_kw: float = Field(..., description="Power level in kW", gt=0)
|
||||
duration_minutes: Optional[int] = Field(None, description="Duration in minutes", gt=0)
|
||||
target_soc: Optional[float] = Field(None, description="Target SOC (0-100%)", ge=0, le=100)
|
||||
|
||||
class BatteryResponse(BaseModel):
|
||||
"""Battery operation response"""
|
||||
battery_id: str
|
||||
status: Dict[str, Any]
|
||||
message: Optional[str] = None
|
||||
|
||||
class BatteryListResponse(BaseModel):
|
||||
"""Response for battery list endpoint"""
|
||||
batteries: List[Dict[str, Any]]
|
||||
count: int
|
||||
total_capacity: float = Field(description="Total system capacity in kWh")
|
||||
total_stored_energy: float = Field(description="Total stored energy in kWh")
|
||||
|
||||
class HistoricalDataRequest(BaseModel):
|
||||
"""Request for historical battery data"""
|
||||
battery_id: str
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
hours: int = Field(default=24, description="Hours of data to retrieve")
|
||||
|
||||
class BatteryHistoricalData(BaseModel):
|
||||
"""Historical battery data point"""
|
||||
timestamp: datetime
|
||||
state_of_charge: float
|
||||
power_kw: float
|
||||
temperature_celsius: Optional[float] = None
|
||||
efficiency: float
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
|
||||
class BatteryAnalytics(BaseModel):
|
||||
"""Battery system analytics"""
|
||||
total_batteries: int
|
||||
active_batteries: int
|
||||
total_capacity_kwh: float
|
||||
total_stored_energy_kwh: float
|
||||
average_soc: float
|
||||
|
||||
# Energy flows
|
||||
total_energy_charged_kwh: float
|
||||
total_energy_discharged_kwh: float
|
||||
net_energy_flow_kwh: float
|
||||
|
||||
# Efficiency metrics
|
||||
round_trip_efficiency: float
|
||||
capacity_utilization: float
|
||||
|
||||
# Health metrics
|
||||
average_health: float
|
||||
batteries_needing_maintenance: int
|
||||
|
||||
class MaintenanceAlert(BaseModel):
|
||||
"""Battery maintenance alert"""
|
||||
battery_id: str
|
||||
alert_type: Literal["scheduled", "health", "temperature", "cycles"]
|
||||
severity: Literal["info", "warning", "critical"]
|
||||
message: str
|
||||
recommended_action: str
|
||||
timestamp: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response"""
|
||||
service: str
|
||||
status: str
|
||||
timestamp: datetime
|
||||
version: str
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
7
microservices/battery-service/requirements.txt
Normal file
7
microservices/battery-service/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
pymongo
|
||||
motor
|
||||
redis
|
||||
python-dotenv
|
||||
pydantic
|
||||
383
microservices/demand-response-service/main.py
Normal file
383
microservices/demand-response-service/main.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
Demand Response Microservice
|
||||
Handles grid interaction, demand response events, and load management.
|
||||
Port: 8003
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from models import (
|
||||
DemandResponseInvitation, InvitationResponse, EventRequest, EventStatus,
|
||||
LoadReductionRequest, FlexibilityResponse, HealthResponse
|
||||
)
|
||||
from database import connect_to_mongo, close_mongo_connection, get_database, connect_to_redis, get_redis
|
||||
from demand_response_service import DemandResponseService
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager"""
|
||||
logger.info("Demand Response Service starting up...")
|
||||
await connect_to_mongo()
|
||||
await connect_to_redis()
|
||||
|
||||
# Start background tasks
|
||||
asyncio.create_task(event_scheduler_task())
|
||||
asyncio.create_task(auto_response_task())
|
||||
|
||||
logger.info("Demand Response Service startup complete")
|
||||
|
||||
yield
|
||||
|
||||
logger.info("Demand Response Service shutting down...")
|
||||
await close_mongo_connection()
|
||||
logger.info("Demand Response Service shutdown complete")
|
||||
|
||||
app = FastAPI(
|
||||
title="Demand Response Service",
|
||||
description="Grid interaction and demand response event management microservice",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Dependencies
|
||||
async def get_db():
|
||||
return await get_database()
|
||||
|
||||
async def get_dr_service(db=Depends(get_db)):
|
||||
redis = await get_redis()
|
||||
return DemandResponseService(db, redis)
|
||||
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
try:
|
||||
db = await get_database()
|
||||
await db.command("ping")
|
||||
|
||||
redis = await get_redis()
|
||||
await redis.ping()
|
||||
|
||||
return HealthResponse(
|
||||
service="demand-response-service",
|
||||
status="healthy",
|
||||
timestamp=datetime.utcnow(),
|
||||
version="1.0.0"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
raise HTTPException(status_code=503, detail="Service Unavailable")
|
||||
|
||||
@app.post("/invitations/send")
|
||||
async def send_invitation(
|
||||
event_request: EventRequest,
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Send demand response invitation to specified IoT devices"""
|
||||
try:
|
||||
result = await service.send_invitation(
|
||||
event_time=event_request.event_time,
|
||||
load_kwh=event_request.load_kwh,
|
||||
load_percentage=event_request.load_percentage,
|
||||
iots=event_request.iots,
|
||||
duration_minutes=event_request.duration_minutes
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Demand response invitation sent successfully",
|
||||
"event_id": result["event_id"],
|
||||
"event_time": event_request.event_time.isoformat(),
|
||||
"participants": len(event_request.iots),
|
||||
"load_kwh": event_request.load_kwh,
|
||||
"load_percentage": event_request.load_percentage
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending invitation: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/invitations/unanswered")
|
||||
async def get_unanswered_invitations(
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Get all unanswered demand response invitations"""
|
||||
try:
|
||||
invitations = await service.get_unanswered_invitations()
|
||||
return {
|
||||
"invitations": invitations,
|
||||
"count": len(invitations)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting unanswered invitations: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/invitations/answered")
|
||||
async def get_answered_invitations(
|
||||
hours: int = 24,
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Get answered demand response invitations"""
|
||||
try:
|
||||
invitations = await service.get_answered_invitations(hours)
|
||||
return {
|
||||
"invitations": invitations,
|
||||
"count": len(invitations),
|
||||
"period_hours": hours
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting answered invitations: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/invitations/answer")
|
||||
async def answer_invitation(
|
||||
response: InvitationResponse,
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Answer a demand response invitation"""
|
||||
try:
|
||||
result = await service.answer_invitation(
|
||||
event_id=response.event_id,
|
||||
iot_id=response.iot_id,
|
||||
response=response.response,
|
||||
committed_reduction_kw=response.committed_reduction_kw
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Invitation response recorded successfully",
|
||||
"event_id": response.event_id,
|
||||
"iot_id": response.iot_id,
|
||||
"response": response.response,
|
||||
"committed_reduction": response.committed_reduction_kw
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error answering invitation: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/invitations/{event_id}")
|
||||
async def get_invitation(
|
||||
event_id: str,
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Get details of a specific demand response invitation"""
|
||||
try:
|
||||
invitation = await service.get_invitation(event_id)
|
||||
if not invitation:
|
||||
raise HTTPException(status_code=404, detail="Invitation not found")
|
||||
|
||||
return invitation
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invitation {event_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/events/schedule")
|
||||
async def schedule_event(
|
||||
event_request: EventRequest,
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Schedule a demand response event"""
|
||||
try:
|
||||
result = await service.schedule_event(
|
||||
event_time=event_request.event_time,
|
||||
iots=event_request.iots,
|
||||
load_reduction_kw=event_request.load_kwh * 1000, # Convert to kW
|
||||
duration_minutes=event_request.duration_minutes
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Demand response event scheduled successfully",
|
||||
"event_id": result["event_id"],
|
||||
"scheduled_time": event_request.event_time.isoformat(),
|
||||
"participants": len(event_request.iots)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error scheduling event: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/events/active")
|
||||
async def get_active_events(
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Get currently active demand response events"""
|
||||
try:
|
||||
events = await service.get_active_events()
|
||||
return {
|
||||
"events": events,
|
||||
"count": len(events)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting active events: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/flexibility/current")
|
||||
async def get_current_flexibility(
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Get current system flexibility capacity"""
|
||||
try:
|
||||
flexibility = await service.get_current_flexibility()
|
||||
return {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"flexibility": flexibility
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting current flexibility: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/flexibility/forecast")
|
||||
async def get_flexibility_forecast(
|
||||
hours: int = 24,
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Get forecasted flexibility for the next specified hours"""
|
||||
try:
|
||||
forecast = await service.get_flexibility_forecast(hours)
|
||||
return {
|
||||
"forecast_hours": hours,
|
||||
"flexibility_forecast": forecast,
|
||||
"generated_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting flexibility forecast: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/load-reduction/execute")
|
||||
async def execute_load_reduction(
|
||||
request: LoadReductionRequest,
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Execute immediate load reduction"""
|
||||
try:
|
||||
result = await service.execute_load_reduction(
|
||||
target_reduction_kw=request.target_reduction_kw,
|
||||
duration_minutes=request.duration_minutes,
|
||||
priority_iots=request.priority_iots
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Load reduction executed successfully",
|
||||
"target_reduction_kw": request.target_reduction_kw,
|
||||
"actual_reduction_kw": result.get("actual_reduction_kw"),
|
||||
"participating_devices": result.get("participating_devices", []),
|
||||
"execution_time": datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing load reduction: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/auto-response/config")
|
||||
async def get_auto_response_config(
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Get auto-response configuration"""
|
||||
try:
|
||||
config = await service.get_auto_response_config()
|
||||
return {"auto_response_config": config}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting auto-response config: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/auto-response/config")
|
||||
async def set_auto_response_config(
|
||||
enabled: bool,
|
||||
max_reduction_percentage: float = 20.0,
|
||||
response_delay_seconds: int = 300,
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Set auto-response configuration"""
|
||||
try:
|
||||
await service.set_auto_response_config(
|
||||
enabled=enabled,
|
||||
max_reduction_percentage=max_reduction_percentage,
|
||||
response_delay_seconds=response_delay_seconds
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Auto-response configuration updated successfully",
|
||||
"enabled": enabled,
|
||||
"max_reduction_percentage": max_reduction_percentage,
|
||||
"response_delay_seconds": response_delay_seconds
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting auto-response config: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/analytics/performance")
|
||||
async def get_performance_analytics(
|
||||
days: int = 30,
|
||||
service: DemandResponseService = Depends(get_dr_service)
|
||||
):
|
||||
"""Get demand response performance analytics"""
|
||||
try:
|
||||
analytics = await service.get_performance_analytics(days)
|
||||
return {
|
||||
"period_days": days,
|
||||
"analytics": analytics,
|
||||
"generated_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting performance analytics: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def event_scheduler_task():
|
||||
"""Background task for scheduling and executing demand response events"""
|
||||
logger.info("Starting event scheduler task")
|
||||
|
||||
while True:
|
||||
try:
|
||||
db = await get_database()
|
||||
redis = await get_redis()
|
||||
service = DemandResponseService(db, redis)
|
||||
|
||||
# Check for events that need to be executed
|
||||
await service.check_scheduled_events()
|
||||
|
||||
# Sleep for 60 seconds between checks
|
||||
await asyncio.sleep(60)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in event scheduler task: {e}")
|
||||
await asyncio.sleep(120) # Wait longer on error
|
||||
|
||||
async def auto_response_task():
|
||||
"""Background task for automatic demand response"""
|
||||
logger.info("Starting auto-response task")
|
||||
|
||||
while True:
|
||||
try:
|
||||
db = await get_database()
|
||||
redis = await get_redis()
|
||||
service = DemandResponseService(db, redis)
|
||||
|
||||
# Check for auto-response opportunities
|
||||
await service.process_auto_responses()
|
||||
|
||||
# Sleep for 30 seconds between checks
|
||||
await asyncio.sleep(30)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auto-response task: {e}")
|
||||
await asyncio.sleep(90)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8003)
|
||||
309
microservices/deploy.sh
Executable file
309
microservices/deploy.sh
Executable file
@@ -0,0 +1,309 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Energy Management Microservices Deployment Script
|
||||
# This script handles deployment, startup, and management of all microservices
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
COMPOSE_FILE="docker-compose.yml"
|
||||
PROJECT_NAME="energy-dashboard"
|
||||
|
||||
# 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 check if Docker and Docker Compose are installed
|
||||
check_dependencies() {
|
||||
print_status "Checking dependencies..."
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker is not installed. Please install Docker first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
print_error "Docker Compose is not installed. Please install Docker Compose first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Dependencies check passed"
|
||||
}
|
||||
|
||||
# Function to create necessary directories and files
|
||||
setup_environment() {
|
||||
print_status "Setting up environment..."
|
||||
|
||||
# Create nginx configuration directory
|
||||
mkdir -p nginx/ssl
|
||||
|
||||
# Create init-mongo directory for database initialization
|
||||
mkdir -p init-mongo
|
||||
|
||||
# Create a simple nginx configuration if it doesn't exist
|
||||
if [ ! -f "nginx/nginx.conf" ]; then
|
||||
cat > nginx/nginx.conf << 'EOF'
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
upstream api_gateway {
|
||||
server api-gateway:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_pass http://api_gateway;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://api_gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
print_success "Created nginx configuration"
|
||||
fi
|
||||
|
||||
# Create MongoDB initialization script if it doesn't exist
|
||||
if [ ! -f "init-mongo/init.js" ]; then
|
||||
cat > init-mongo/init.js << 'EOF'
|
||||
// MongoDB initialization script
|
||||
db = db.getSiblingDB('energy_dashboard');
|
||||
db.createUser({
|
||||
user: 'dashboard_user',
|
||||
pwd: 'dashboard_pass',
|
||||
roles: [
|
||||
{ role: 'readWrite', db: 'energy_dashboard' },
|
||||
{ role: 'readWrite', db: 'energy_dashboard_tokens' },
|
||||
{ role: 'readWrite', db: 'energy_dashboard_batteries' },
|
||||
{ role: 'readWrite', db: 'energy_dashboard_demand_response' },
|
||||
{ role: 'readWrite', db: 'energy_dashboard_p2p' },
|
||||
{ role: 'readWrite', db: 'energy_dashboard_forecasting' },
|
||||
{ role: 'readWrite', db: 'energy_dashboard_iot' }
|
||||
]
|
||||
});
|
||||
|
||||
// Create initial collections and indexes
|
||||
db.sensors.createIndex({ "sensor_id": 1 }, { unique: true });
|
||||
db.sensor_readings.createIndex({ "sensor_id": 1, "timestamp": -1 });
|
||||
db.room_metrics.createIndex({ "room": 1, "timestamp": -1 });
|
||||
|
||||
print("MongoDB initialization completed");
|
||||
EOF
|
||||
print_success "Created MongoDB initialization script"
|
||||
fi
|
||||
|
||||
print_success "Environment setup completed"
|
||||
}
|
||||
|
||||
# Function to build all services
|
||||
build_services() {
|
||||
print_status "Building all microservices..."
|
||||
|
||||
docker-compose -f $COMPOSE_FILE build
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "All services built successfully"
|
||||
else
|
||||
print_error "Failed to build services"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to start all services
|
||||
start_services() {
|
||||
print_status "Starting all services..."
|
||||
|
||||
docker-compose -f $COMPOSE_FILE up -d
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "All services started successfully"
|
||||
else
|
||||
print_error "Failed to start services"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to stop all services
|
||||
stop_services() {
|
||||
print_status "Stopping all services..."
|
||||
|
||||
docker-compose -f $COMPOSE_FILE down
|
||||
|
||||
print_success "All services stopped"
|
||||
}
|
||||
|
||||
# Function to restart all services
|
||||
restart_services() {
|
||||
stop_services
|
||||
start_services
|
||||
}
|
||||
|
||||
# Function to show service status
|
||||
show_status() {
|
||||
print_status "Service status:"
|
||||
docker-compose -f $COMPOSE_FILE ps
|
||||
|
||||
print_status "Service health checks:"
|
||||
|
||||
# Wait a moment for services to start
|
||||
sleep 5
|
||||
|
||||
services=("api-gateway:8000" "token-service:8001" "battery-service:8002" "demand-response-service:8003")
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
name="${service%:*}"
|
||||
port="${service#*:}"
|
||||
|
||||
if curl -f -s "http://localhost:$port/health" > /dev/null; then
|
||||
print_success "$name is healthy"
|
||||
else
|
||||
print_warning "$name is not responding to health checks"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Function to view logs
|
||||
view_logs() {
|
||||
if [ -z "$2" ]; then
|
||||
print_status "Showing logs for all services..."
|
||||
docker-compose -f $COMPOSE_FILE logs -f
|
||||
else
|
||||
print_status "Showing logs for $2..."
|
||||
docker-compose -f $COMPOSE_FILE logs -f $2
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to clean up everything
|
||||
cleanup() {
|
||||
print_warning "This will remove all containers, images, and volumes. Are you sure? (y/N)"
|
||||
read -r response
|
||||
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
|
||||
print_status "Cleaning up everything..."
|
||||
docker-compose -f $COMPOSE_FILE down -v --rmi all
|
||||
docker system prune -f
|
||||
print_success "Cleanup completed"
|
||||
else
|
||||
print_status "Cleanup cancelled"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to run database migrations or setup
|
||||
setup_database() {
|
||||
print_status "Setting up databases..."
|
||||
|
||||
# Wait for MongoDB to be ready
|
||||
print_status "Waiting for MongoDB to be ready..."
|
||||
sleep 10
|
||||
|
||||
# Run any additional setup scripts here
|
||||
print_success "Database setup completed"
|
||||
}
|
||||
|
||||
# Function to show help
|
||||
show_help() {
|
||||
echo "Energy Management Microservices Deployment Script"
|
||||
echo ""
|
||||
echo "Usage: $0 [COMMAND]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " setup Setup environment and dependencies"
|
||||
echo " build Build all microservices"
|
||||
echo " start Start all services"
|
||||
echo " stop Stop all services"
|
||||
echo " restart Restart all services"
|
||||
echo " status Show service status and health"
|
||||
echo " logs Show logs for all services"
|
||||
echo " logs <svc> Show logs for specific service"
|
||||
echo " deploy Full deployment (setup + build + start)"
|
||||
echo " db-setup Setup databases"
|
||||
echo " cleanup Remove all containers, images, and volumes"
|
||||
echo " help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 deploy # Full deployment"
|
||||
echo " $0 logs battery-service # Show battery service logs"
|
||||
echo " $0 status # Check service health"
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
case "${1:-help}" in
|
||||
setup)
|
||||
check_dependencies
|
||||
setup_environment
|
||||
;;
|
||||
build)
|
||||
check_dependencies
|
||||
build_services
|
||||
;;
|
||||
start)
|
||||
check_dependencies
|
||||
start_services
|
||||
;;
|
||||
stop)
|
||||
stop_services
|
||||
;;
|
||||
restart)
|
||||
restart_services
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
logs)
|
||||
view_logs $@
|
||||
;;
|
||||
deploy)
|
||||
check_dependencies
|
||||
setup_environment
|
||||
build_services
|
||||
start_services
|
||||
setup_database
|
||||
show_status
|
||||
;;
|
||||
db-setup)
|
||||
setup_database
|
||||
;;
|
||||
cleanup)
|
||||
cleanup
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown command: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
193
microservices/docker-compose.yml
Normal file
193
microservices/docker-compose.yml
Normal file
@@ -0,0 +1,193 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Database Services
|
||||
mongodb:
|
||||
image: mongo:5.0
|
||||
container_name: energy-mongodb
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: admin
|
||||
MONGO_INITDB_ROOT_PASSWORD: password123
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
- ./init-mongo:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: energy-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
# API Gateway
|
||||
api-gateway:
|
||||
build:
|
||||
context: ./api-gateway
|
||||
dockerfile: Dockerfile
|
||||
container_name: energy-api-gateway
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard?authSource=admin
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- TOKEN_SERVICE_URL=http://token-service:8001
|
||||
- BATTERY_SERVICE_URL=http://battery-service:8002
|
||||
- DEMAND_RESPONSE_SERVICE_URL=http://demand-response-service:8003
|
||||
- P2P_TRADING_SERVICE_URL=http://p2p-trading-service:8004
|
||||
- FORECASTING_SERVICE_URL=http://forecasting-service:8005
|
||||
- IOT_CONTROL_SERVICE_URL=http://iot-control-service:8006
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
- token-service
|
||||
- battery-service
|
||||
- demand-response-service
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
# Token Management Service
|
||||
token-service:
|
||||
build:
|
||||
context: ./token-service
|
||||
dockerfile: Dockerfile
|
||||
container_name: energy-token-service
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8001:8001"
|
||||
environment:
|
||||
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_tokens?authSource=admin
|
||||
- JWT_SECRET_KEY=your-super-secret-jwt-key-change-in-production
|
||||
depends_on:
|
||||
- mongodb
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
# Battery Management Service
|
||||
battery-service:
|
||||
build:
|
||||
context: ./battery-service
|
||||
dockerfile: Dockerfile
|
||||
container_name: energy-battery-service
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8002:8002"
|
||||
environment:
|
||||
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_batteries?authSource=admin
|
||||
- REDIS_URL=redis://redis:6379
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
# Demand Response Service
|
||||
demand-response-service:
|
||||
build:
|
||||
context: ./demand-response-service
|
||||
dockerfile: Dockerfile
|
||||
container_name: energy-demand-response-service
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8003:8003"
|
||||
environment:
|
||||
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_demand_response?authSource=admin
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- IOT_CONTROL_SERVICE_URL=http://iot-control-service:8006
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
# P2P Trading Service
|
||||
p2p-trading-service:
|
||||
build:
|
||||
context: ./p2p-trading-service
|
||||
dockerfile: Dockerfile
|
||||
container_name: energy-p2p-trading-service
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8004:8004"
|
||||
environment:
|
||||
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_p2p?authSource=admin
|
||||
- REDIS_URL=redis://redis:6379
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
# Forecasting Service
|
||||
forecasting-service:
|
||||
build:
|
||||
context: ./forecasting-service
|
||||
dockerfile: Dockerfile
|
||||
container_name: energy-forecasting-service
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8005:8005"
|
||||
environment:
|
||||
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_forecasting?authSource=admin
|
||||
- REDIS_URL=redis://redis:6379
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
# IoT Control Service
|
||||
iot-control-service:
|
||||
build:
|
||||
context: ./iot-control-service
|
||||
dockerfile: Dockerfile
|
||||
container_name: energy-iot-control-service
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8006:8006"
|
||||
environment:
|
||||
- MONGO_URL=mongodb://admin:password123@mongodb:27017/energy_dashboard_iot?authSource=admin
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- BATTERY_SERVICE_URL=http://battery-service:8002
|
||||
- DEMAND_RESPONSE_SERVICE_URL=http://demand-response-service:8003
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
# Monitoring and Management
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: energy-nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./nginx/ssl:/etc/nginx/ssl
|
||||
depends_on:
|
||||
- api-gateway
|
||||
networks:
|
||||
- energy-network
|
||||
|
||||
networks:
|
||||
energy-network:
|
||||
driver: bridge
|
||||
name: energy-network
|
||||
|
||||
volumes:
|
||||
mongodb_data:
|
||||
name: energy-mongodb-data
|
||||
redis_data:
|
||||
name: energy-redis-data
|
||||
25
microservices/token-service/Dockerfile
Normal file
25
microservices/token-service/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8001
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8001/health || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "main.py"]
|
||||
65
microservices/token-service/database.py
Normal file
65
microservices/token-service/database.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Database connection for Token Service
|
||||
"""
|
||||
|
||||
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
||||
import logging
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Database configuration
|
||||
MONGO_URL = os.getenv("MONGO_URL", "mongodb://localhost:27017")
|
||||
DATABASE_NAME = os.getenv("DATABASE_NAME", "energy_dashboard_tokens")
|
||||
|
||||
# Global database client
|
||||
_client: AsyncIOMotorClient = None
|
||||
_database: AsyncIOMotorDatabase = None
|
||||
|
||||
async def connect_to_mongo():
|
||||
"""Create database connection"""
|
||||
global _client, _database
|
||||
|
||||
try:
|
||||
_client = AsyncIOMotorClient(MONGO_URL)
|
||||
_database = _client[DATABASE_NAME]
|
||||
|
||||
# Test connection
|
||||
await _database.command("ping")
|
||||
logger.info(f"Connected to MongoDB: {DATABASE_NAME}")
|
||||
|
||||
# Create indexes for performance
|
||||
await create_indexes()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to MongoDB: {e}")
|
||||
raise
|
||||
|
||||
async def close_mongo_connection():
|
||||
"""Close database connection"""
|
||||
global _client
|
||||
|
||||
if _client:
|
||||
_client.close()
|
||||
logger.info("Disconnected from MongoDB")
|
||||
|
||||
async def get_database() -> AsyncIOMotorDatabase:
|
||||
"""Get database instance"""
|
||||
global _database
|
||||
|
||||
if _database is None:
|
||||
raise RuntimeError("Database not initialized. Call connect_to_mongo() first.")
|
||||
|
||||
return _database
|
||||
|
||||
async def create_indexes():
|
||||
"""Create database indexes for performance"""
|
||||
db = await get_database()
|
||||
|
||||
# Indexes for tokens collection
|
||||
await db.tokens.create_index("token", unique=True)
|
||||
await db.tokens.create_index("active")
|
||||
await db.tokens.create_index("expires_at")
|
||||
await db.tokens.create_index("name")
|
||||
|
||||
logger.info("Database indexes created")
|
||||
190
microservices/token-service/main.py
Normal file
190
microservices/token-service/main.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Token Management Microservice
|
||||
Handles JWT authentication, token generation, validation, and resource access control.
|
||||
Port: 8001
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, HTTPException, Depends, Security
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from models import (
|
||||
TokenGenerateRequest, TokenResponse, TokenValidationResponse,
|
||||
TokenListResponse, HealthResponse
|
||||
)
|
||||
from database import connect_to_mongo, close_mongo_connection, get_database
|
||||
from token_service import TokenService
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager"""
|
||||
logger.info("Token Service starting up...")
|
||||
await connect_to_mongo()
|
||||
logger.info("Token Service startup complete")
|
||||
|
||||
yield
|
||||
|
||||
logger.info("Token Service shutting down...")
|
||||
await close_mongo_connection()
|
||||
logger.info("Token Service shutdown complete")
|
||||
|
||||
app = FastAPI(
|
||||
title="Token Management Service",
|
||||
description="JWT authentication and token management microservice",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Dependency for database
|
||||
async def get_db():
|
||||
return await get_database()
|
||||
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
try:
|
||||
db = await get_database()
|
||||
await db.command("ping")
|
||||
|
||||
return HealthResponse(
|
||||
service="token-service",
|
||||
status="healthy",
|
||||
timestamp=datetime.utcnow(),
|
||||
version="1.0.0"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
raise HTTPException(status_code=503, detail="Service Unavailable")
|
||||
|
||||
@app.get("/tokens", response_model=TokenListResponse)
|
||||
async def get_tokens(db=Depends(get_db)):
|
||||
"""Get all tokens"""
|
||||
try:
|
||||
token_service = TokenService(db)
|
||||
tokens = await token_service.get_tokens()
|
||||
|
||||
return TokenListResponse(
|
||||
tokens=tokens,
|
||||
count=len(tokens)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting tokens: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/tokens/generate", response_model=TokenResponse)
|
||||
async def generate_token(request: TokenGenerateRequest, db=Depends(get_db)):
|
||||
"""Generate a new JWT token"""
|
||||
try:
|
||||
token_service = TokenService(db)
|
||||
token = token_service.generate_token(
|
||||
name=request.name,
|
||||
list_of_resources=request.list_of_resources,
|
||||
data_aggregation=request.data_aggregation,
|
||||
time_aggregation=request.time_aggregation,
|
||||
embargo=request.embargo,
|
||||
exp_hours=request.exp_hours
|
||||
)
|
||||
|
||||
return TokenResponse(token=token)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating token: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/tokens/validate", response_model=TokenValidationResponse)
|
||||
async def validate_token(token: str, db=Depends(get_db)):
|
||||
"""Validate and decode a JWT token"""
|
||||
try:
|
||||
token_service = TokenService(db)
|
||||
is_valid = await token_service.is_token_valid(token)
|
||||
decoded = token_service.decode_token(token) if is_valid else None
|
||||
|
||||
return TokenValidationResponse(
|
||||
valid=is_valid,
|
||||
token=token,
|
||||
decoded=decoded if is_valid and "error" not in (decoded or {}) else None,
|
||||
error=decoded.get("error") if decoded and "error" in decoded else None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating token: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/tokens/save")
|
||||
async def save_token(token: str, db=Depends(get_db)):
|
||||
"""Save a token to database"""
|
||||
try:
|
||||
token_service = TokenService(db)
|
||||
result = await token_service.insert_token(token)
|
||||
return result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving token: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/tokens/revoke")
|
||||
async def revoke_token(token: str, db=Depends(get_db)):
|
||||
"""Revoke a token"""
|
||||
try:
|
||||
token_service = TokenService(db)
|
||||
result = await token_service.revoke_token(token)
|
||||
return result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error revoking token: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/tokens/{token}/permissions")
|
||||
async def get_token_permissions(token: str, db=Depends(get_db)):
|
||||
"""Get permissions for a specific token"""
|
||||
try:
|
||||
token_service = TokenService(db)
|
||||
permissions = await token_service.get_token_permissions(token)
|
||||
|
||||
if permissions:
|
||||
return {"permissions": permissions}
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting token permissions: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.delete("/tokens/cleanup")
|
||||
async def cleanup_expired_tokens(db=Depends(get_db)):
|
||||
"""Clean up expired tokens"""
|
||||
try:
|
||||
token_service = TokenService(db)
|
||||
expired_count = await token_service.cleanup_expired_tokens()
|
||||
|
||||
return {
|
||||
"message": "Expired tokens cleaned up",
|
||||
"expired_tokens_removed": expired_count
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up tokens: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||
55
microservices/token-service/models.py
Normal file
55
microservices/token-service/models.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Pydantic models for Token Management Service
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
class TokenGenerateRequest(BaseModel):
|
||||
"""Request model for token generation"""
|
||||
name: str = Field(..., description="Token owner name")
|
||||
list_of_resources: List[str] = Field(..., description="List of accessible resources")
|
||||
data_aggregation: bool = Field(default=False, description="Allow data aggregation")
|
||||
time_aggregation: bool = Field(default=False, description="Allow time aggregation")
|
||||
embargo: int = Field(default=0, description="Embargo period in seconds")
|
||||
exp_hours: int = Field(default=24, description="Token expiration in hours")
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""Response model for token operations"""
|
||||
token: str = Field(..., description="JWT token")
|
||||
|
||||
class TokenValidationResponse(BaseModel):
|
||||
"""Response model for token validation"""
|
||||
valid: bool = Field(..., description="Whether token is valid")
|
||||
token: str = Field(..., description="Original token")
|
||||
decoded: Optional[Dict[str, Any]] = Field(None, description="Decoded token payload")
|
||||
error: Optional[str] = Field(None, description="Error message if invalid")
|
||||
|
||||
class TokenRecord(BaseModel):
|
||||
"""Token database record model"""
|
||||
token: str
|
||||
datetime: str
|
||||
active: bool
|
||||
name: str
|
||||
resources: List[str]
|
||||
expires_at: str
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
class TokenListResponse(BaseModel):
|
||||
"""Response model for token list"""
|
||||
tokens: List[Dict[str, Any]]
|
||||
count: int
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response"""
|
||||
service: str
|
||||
status: str
|
||||
timestamp: datetime
|
||||
version: str
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
7
microservices/token-service/requirements.txt
Normal file
7
microservices/token-service/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
pymongo
|
||||
motor
|
||||
PyJWT
|
||||
python-dotenv
|
||||
pydantic
|
||||
157
microservices/token-service/token_service.py
Normal file
157
microservices/token-service/token_service.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Token service implementation
|
||||
"""
|
||||
|
||||
import jwt
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
from motor.motor_asyncio import AsyncIOMotorDatabase
|
||||
import os
|
||||
|
||||
class TokenService:
|
||||
"""Service for managing JWT tokens and authentication"""
|
||||
|
||||
def __init__(self, db: AsyncIOMotorDatabase, secret_key: str = None):
|
||||
self.db = db
|
||||
self.secret_key = secret_key or os.getenv("JWT_SECRET_KEY", "energy-dashboard-secret-key")
|
||||
self.tokens_collection = db.tokens
|
||||
|
||||
def generate_token(self, name: str, list_of_resources: List[str],
|
||||
data_aggregation: bool = False, time_aggregation: bool = False,
|
||||
embargo: int = 0, exp_hours: int = 24) -> str:
|
||||
"""Generate a new JWT token with specified permissions"""
|
||||
|
||||
# Calculate expiration time
|
||||
exp_timestamp = int((datetime.utcnow() + timedelta(hours=exp_hours)).timestamp())
|
||||
|
||||
# Create token payload
|
||||
payload = {
|
||||
"name": name,
|
||||
"list_of_resources": list_of_resources,
|
||||
"data_aggregation": data_aggregation,
|
||||
"time_aggregation": time_aggregation,
|
||||
"embargo": embargo,
|
||||
"exp": exp_timestamp,
|
||||
"iat": int(datetime.utcnow().timestamp()),
|
||||
"jti": str(uuid.uuid4()) # unique token ID
|
||||
}
|
||||
|
||||
# Generate JWT token
|
||||
token = jwt.encode(payload, self.secret_key, algorithm="HS256")
|
||||
return token
|
||||
|
||||
def decode_token(self, token: str) -> Optional[Dict[str, Any]]:
|
||||
"""Decode and verify JWT token"""
|
||||
try:
|
||||
payload = jwt.decode(token, self.secret_key, algorithms=["HS256"])
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
return {"error": "Token has expired"}
|
||||
except jwt.InvalidTokenError:
|
||||
return {"error": "Invalid token"}
|
||||
|
||||
async def insert_token(self, token: str) -> Dict[str, Any]:
|
||||
"""Save token to database"""
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Decode token to verify it's valid
|
||||
decoded = self.decode_token(token)
|
||||
if decoded and "error" not in decoded:
|
||||
token_record = {
|
||||
"token": token,
|
||||
"datetime": now,
|
||||
"active": True,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"name": decoded.get("name", ""),
|
||||
"resources": decoded.get("list_of_resources", []),
|
||||
"expires_at": datetime.fromtimestamp(decoded.get("exp", 0))
|
||||
}
|
||||
|
||||
# Upsert token (update if exists, insert if not)
|
||||
await self.tokens_collection.replace_one(
|
||||
{"token": token},
|
||||
token_record,
|
||||
upsert=True
|
||||
)
|
||||
|
||||
return {
|
||||
"token": token,
|
||||
"datetime": now.isoformat(),
|
||||
"active": True
|
||||
}
|
||||
else:
|
||||
raise ValueError("Invalid token cannot be saved")
|
||||
|
||||
async def revoke_token(self, token: str) -> Dict[str, Any]:
|
||||
"""Revoke a token by marking it as inactive"""
|
||||
now = datetime.utcnow()
|
||||
|
||||
result = await self.tokens_collection.update_one(
|
||||
{"token": token},
|
||||
{
|
||||
"$set": {
|
||||
"active": False,
|
||||
"updated_at": now,
|
||||
"revoked_at": now
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if result.matched_count > 0:
|
||||
return {
|
||||
"token": token,
|
||||
"datetime": now.isoformat(),
|
||||
"active": False
|
||||
}
|
||||
else:
|
||||
raise ValueError("Token not found")
|
||||
|
||||
async def get_tokens(self) -> List[Dict[str, Any]]:
|
||||
"""Get all tokens from database"""
|
||||
cursor = self.tokens_collection.find({})
|
||||
tokens = []
|
||||
|
||||
async for token_record in cursor:
|
||||
# Convert ObjectId to string and datetime to ISO format
|
||||
token_record["_id"] = str(token_record["_id"])
|
||||
for field in ["datetime", "created_at", "updated_at", "expires_at", "revoked_at"]:
|
||||
if field in token_record and token_record[field]:
|
||||
token_record[field] = token_record[field].isoformat()
|
||||
|
||||
tokens.append(token_record)
|
||||
|
||||
return tokens
|
||||
|
||||
async def is_token_valid(self, token: str) -> bool:
|
||||
"""Check if token is valid and active"""
|
||||
# Check if token exists and is active in database
|
||||
token_record = await self.tokens_collection.find_one({
|
||||
"token": token,
|
||||
"active": True
|
||||
})
|
||||
|
||||
if not token_record:
|
||||
return False
|
||||
|
||||
# Verify JWT signature and expiration
|
||||
decoded = self.decode_token(token)
|
||||
return decoded is not None and "error" not in decoded
|
||||
|
||||
async def get_token_permissions(self, token: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get permissions for a valid token"""
|
||||
if await self.is_token_valid(token):
|
||||
return self.decode_token(token)
|
||||
return None
|
||||
|
||||
async def cleanup_expired_tokens(self) -> int:
|
||||
"""Remove expired tokens from database"""
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Delete tokens that have expired
|
||||
result = await self.tokens_collection.delete_many({
|
||||
"expires_at": {"$lt": now}
|
||||
})
|
||||
|
||||
return result.deleted_count
|
||||
Reference in New Issue
Block a user