first commit

This commit is contained in:
rafaeldpsilva
2025-09-09 13:46:42 +01:00
commit a7a18e6295
77 changed files with 8678 additions and 0 deletions

View 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
View 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)
```

View 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"]

View 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")
}

View 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

View 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)

View 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

View File

@@ -0,0 +1,5 @@
fastapi
uvicorn[standard]
aiohttp
python-dotenv
pydantic

View 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

View 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"]

View 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)

View 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")

View 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)

View 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()
}

View File

@@ -0,0 +1,7 @@
fastapi
uvicorn[standard]
pymongo
motor
redis
python-dotenv
pydantic

View 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
View 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

View 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

View 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"]

View 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")

View 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)

View 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()
}

View File

@@ -0,0 +1,7 @@
fastapi
uvicorn[standard]
pymongo
motor
PyJWT
python-dotenv
pydantic

View 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