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