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