first commit
This commit is contained in:
26
microservices/battery-service/Dockerfile
Normal file
26
microservices/battery-service/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8002
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8002/health || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "main.py"]
|
||||
414
microservices/battery-service/battery_service.py
Normal file
414
microservices/battery-service/battery_service.py
Normal file
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
Battery management service implementation
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Any
|
||||
from motor.motor_asyncio import AsyncIOMotorDatabase
|
||||
import redis.asyncio as redis
|
||||
import logging
|
||||
import json
|
||||
|
||||
from models import BatteryState, BatteryType, MaintenanceAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BatteryService:
|
||||
"""Service for managing battery operations and monitoring"""
|
||||
|
||||
def __init__(self, db: AsyncIOMotorDatabase, redis_client: redis.Redis):
|
||||
self.db = db
|
||||
self.redis = redis_client
|
||||
self.batteries_collection = db.batteries
|
||||
self.battery_history_collection = db.battery_history
|
||||
self.maintenance_alerts_collection = db.maintenance_alerts
|
||||
|
||||
async def get_batteries(self) -> List[Dict[str, Any]]:
|
||||
"""Get all registered batteries"""
|
||||
cursor = self.batteries_collection.find({})
|
||||
batteries = []
|
||||
|
||||
async for battery in cursor:
|
||||
battery["_id"] = str(battery["_id"])
|
||||
# Convert datetime fields to ISO format
|
||||
for field in ["installed_date", "last_maintenance", "next_maintenance", "last_updated"]:
|
||||
if field in battery and battery[field]:
|
||||
battery[field] = battery[field].isoformat()
|
||||
|
||||
batteries.append(battery)
|
||||
|
||||
return batteries
|
||||
|
||||
async def get_battery_status(self, battery_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get current status of a specific battery"""
|
||||
# First try to get from Redis cache
|
||||
cached_status = await self.redis.get(f"battery:status:{battery_id}")
|
||||
if cached_status:
|
||||
return json.loads(cached_status)
|
||||
|
||||
# Fall back to database
|
||||
battery = await self.batteries_collection.find_one({"battery_id": battery_id})
|
||||
if battery:
|
||||
battery["_id"] = str(battery["_id"])
|
||||
|
||||
# Convert datetime fields
|
||||
for field in ["installed_date", "last_maintenance", "next_maintenance", "last_updated"]:
|
||||
if field in battery and battery[field]:
|
||||
battery[field] = battery[field].isoformat()
|
||||
|
||||
# Cache the result
|
||||
await self.redis.setex(
|
||||
f"battery:status:{battery_id}",
|
||||
300, # 5 minutes TTL
|
||||
json.dumps(battery, default=str)
|
||||
)
|
||||
|
||||
return battery
|
||||
|
||||
return None
|
||||
|
||||
async def charge_battery(self, battery_id: str, power_kw: float, duration_minutes: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""Initiate battery charging"""
|
||||
battery = await self.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
return {"success": False, "error": "Battery not found"}
|
||||
|
||||
# Check if battery can accept charge
|
||||
current_soc = battery.get("state_of_charge", 0)
|
||||
max_charge_power = battery.get("max_charge_power_kw", 0)
|
||||
|
||||
if current_soc >= 100:
|
||||
return {"success": False, "error": "Battery is already fully charged"}
|
||||
|
||||
if power_kw > max_charge_power:
|
||||
return {"success": False, "error": f"Requested power ({power_kw} kW) exceeds maximum charge power ({max_charge_power} kW)"}
|
||||
|
||||
# Update battery state
|
||||
now = datetime.utcnow()
|
||||
update_data = {
|
||||
"state": BatteryState.CHARGING.value,
|
||||
"current_power_kw": power_kw,
|
||||
"last_updated": now
|
||||
}
|
||||
|
||||
if duration_minutes:
|
||||
update_data["charging_until"] = now + timedelta(minutes=duration_minutes)
|
||||
|
||||
await self.batteries_collection.update_one(
|
||||
{"battery_id": battery_id},
|
||||
{"$set": update_data}
|
||||
)
|
||||
|
||||
# Clear cache
|
||||
await self.redis.delete(f"battery:status:{battery_id}")
|
||||
|
||||
# Log the charging event
|
||||
await self._log_battery_event(battery_id, "charging_started", {
|
||||
"power_kw": power_kw,
|
||||
"duration_minutes": duration_minutes
|
||||
})
|
||||
|
||||
# Publish event to Redis for real-time updates
|
||||
await self.redis.publish("battery_events", json.dumps({
|
||||
"event": "charging_started",
|
||||
"battery_id": battery_id,
|
||||
"power_kw": power_kw,
|
||||
"timestamp": now.isoformat()
|
||||
}))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"estimated_completion": (now + timedelta(minutes=duration_minutes)).isoformat() if duration_minutes else None
|
||||
}
|
||||
|
||||
async def discharge_battery(self, battery_id: str, power_kw: float, duration_minutes: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""Initiate battery discharging"""
|
||||
battery = await self.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
return {"success": False, "error": "Battery not found"}
|
||||
|
||||
# Check if battery can discharge
|
||||
current_soc = battery.get("state_of_charge", 0)
|
||||
max_discharge_power = battery.get("max_discharge_power_kw", 0)
|
||||
|
||||
if current_soc <= 0:
|
||||
return {"success": False, "error": "Battery is already empty"}
|
||||
|
||||
if power_kw > max_discharge_power:
|
||||
return {"success": False, "error": f"Requested power ({power_kw} kW) exceeds maximum discharge power ({max_discharge_power} kW)"}
|
||||
|
||||
# Update battery state
|
||||
now = datetime.utcnow()
|
||||
update_data = {
|
||||
"state": BatteryState.DISCHARGING.value,
|
||||
"current_power_kw": -power_kw, # Negative for discharging
|
||||
"last_updated": now
|
||||
}
|
||||
|
||||
if duration_minutes:
|
||||
update_data["discharging_until"] = now + timedelta(minutes=duration_minutes)
|
||||
|
||||
await self.batteries_collection.update_one(
|
||||
{"battery_id": battery_id},
|
||||
{"$set": update_data}
|
||||
)
|
||||
|
||||
# Clear cache
|
||||
await self.redis.delete(f"battery:status:{battery_id}")
|
||||
|
||||
# Log the discharging event
|
||||
await self._log_battery_event(battery_id, "discharging_started", {
|
||||
"power_kw": power_kw,
|
||||
"duration_minutes": duration_minutes
|
||||
})
|
||||
|
||||
# Publish event
|
||||
await self.redis.publish("battery_events", json.dumps({
|
||||
"event": "discharging_started",
|
||||
"battery_id": battery_id,
|
||||
"power_kw": power_kw,
|
||||
"timestamp": now.isoformat()
|
||||
}))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"estimated_completion": (now + timedelta(minutes=duration_minutes)).isoformat() if duration_minutes else None
|
||||
}
|
||||
|
||||
async def optimize_battery(self, battery_id: str, target_soc: float) -> Dict[str, Any]:
|
||||
"""Optimize battery charging/discharging to reach target SOC"""
|
||||
battery = await self.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
return {"success": False, "error": "Battery not found"}
|
||||
|
||||
current_soc = battery.get("state_of_charge", 0)
|
||||
capacity_kwh = battery.get("capacity_kwh", 0)
|
||||
|
||||
# Calculate energy needed
|
||||
energy_difference_kwh = (target_soc - current_soc) / 100 * capacity_kwh
|
||||
|
||||
if abs(energy_difference_kwh) < 0.1: # Within 0.1 kWh
|
||||
return {"message": "Battery is already at target SOC", "action": "none"}
|
||||
|
||||
if energy_difference_kwh > 0:
|
||||
# Need to charge
|
||||
max_power = battery.get("max_charge_power_kw", 0)
|
||||
action = "charge"
|
||||
else:
|
||||
# Need to discharge
|
||||
max_power = battery.get("max_discharge_power_kw", 0)
|
||||
action = "discharge"
|
||||
energy_difference_kwh = abs(energy_difference_kwh)
|
||||
|
||||
# Calculate optimal power and duration
|
||||
optimal_power = min(max_power, energy_difference_kwh * 2) # Conservative power level
|
||||
duration_hours = energy_difference_kwh / optimal_power
|
||||
duration_minutes = int(duration_hours * 60)
|
||||
|
||||
# Execute the optimization
|
||||
if action == "charge":
|
||||
result = await self.charge_battery(battery_id, optimal_power, duration_minutes)
|
||||
else:
|
||||
result = await self.discharge_battery(battery_id, optimal_power, duration_minutes)
|
||||
|
||||
return {
|
||||
"action": action,
|
||||
"power_kw": optimal_power,
|
||||
"duration_minutes": duration_minutes,
|
||||
"energy_difference_kwh": energy_difference_kwh,
|
||||
"result": result
|
||||
}
|
||||
|
||||
async def get_battery_history(self, battery_id: str, hours: int = 24) -> List[Dict[str, Any]]:
|
||||
"""Get historical data for a battery"""
|
||||
start_time = datetime.utcnow() - timedelta(hours=hours)
|
||||
|
||||
cursor = self.battery_history_collection.find({
|
||||
"battery_id": battery_id,
|
||||
"timestamp": {"$gte": start_time}
|
||||
}).sort("timestamp", -1)
|
||||
|
||||
history = []
|
||||
async for record in cursor:
|
||||
record["_id"] = str(record["_id"])
|
||||
if "timestamp" in record:
|
||||
record["timestamp"] = record["timestamp"].isoformat()
|
||||
history.append(record)
|
||||
|
||||
return history
|
||||
|
||||
async def get_battery_analytics(self, hours: int = 24) -> Dict[str, Any]:
|
||||
"""Get system-wide battery analytics"""
|
||||
start_time = datetime.utcnow() - timedelta(hours=hours)
|
||||
|
||||
# Get all batteries
|
||||
batteries = await self.get_batteries()
|
||||
|
||||
total_capacity = sum(b.get("capacity_kwh", 0) for b in batteries)
|
||||
total_stored = sum(b.get("stored_energy_kwh", 0) for b in batteries)
|
||||
active_count = sum(1 for b in batteries if b.get("state") != "error")
|
||||
|
||||
# Aggregate historical data
|
||||
pipeline = [
|
||||
{"$match": {"timestamp": {"$gte": start_time}}},
|
||||
{"$group": {
|
||||
"_id": None,
|
||||
"total_energy_charged": {"$sum": {"$cond": [{"$gt": ["$power_kw", 0]}, {"$multiply": ["$power_kw", 0.5]}, 0]}}, # Approximate kWh
|
||||
"total_energy_discharged": {"$sum": {"$cond": [{"$lt": ["$power_kw", 0]}, {"$multiply": [{"$abs": "$power_kw"}, 0.5]}, 0]}},
|
||||
"avg_efficiency": {"$avg": "$efficiency"}
|
||||
}}
|
||||
]
|
||||
|
||||
cursor = self.battery_history_collection.aggregate(pipeline)
|
||||
analytics_data = await cursor.to_list(length=1)
|
||||
|
||||
if analytics_data:
|
||||
energy_data = analytics_data[0]
|
||||
else:
|
||||
energy_data = {
|
||||
"total_energy_charged": 0,
|
||||
"total_energy_discharged": 0,
|
||||
"avg_efficiency": 0.95
|
||||
}
|
||||
|
||||
# Calculate metrics
|
||||
average_soc = sum(b.get("state_of_charge", 0) for b in batteries) / len(batteries) if batteries else 0
|
||||
average_health = sum(b.get("health_percentage", 100) for b in batteries) / len(batteries) if batteries else 100
|
||||
|
||||
return {
|
||||
"total_batteries": len(batteries),
|
||||
"active_batteries": active_count,
|
||||
"total_capacity_kwh": total_capacity,
|
||||
"total_stored_energy_kwh": total_stored,
|
||||
"average_soc": round(average_soc, 2),
|
||||
"total_energy_charged_kwh": round(energy_data["total_energy_charged"], 2),
|
||||
"total_energy_discharged_kwh": round(energy_data["total_energy_discharged"], 2),
|
||||
"net_energy_flow_kwh": round(energy_data["total_energy_charged"] - energy_data["total_energy_discharged"], 2),
|
||||
"round_trip_efficiency": round(energy_data.get("avg_efficiency", 0.95) * 100, 2),
|
||||
"capacity_utilization": round((total_stored / total_capacity * 100) if total_capacity > 0 else 0, 2),
|
||||
"average_health": round(average_health, 2),
|
||||
"batteries_needing_maintenance": sum(1 for b in batteries if b.get("health_percentage", 100) < 80)
|
||||
}
|
||||
|
||||
async def update_battery_status(self, battery_id: str):
|
||||
"""Update battery status with simulated or real data"""
|
||||
# This would typically connect to actual battery management systems
|
||||
# For now, we'll simulate some basic updates
|
||||
|
||||
battery = await self.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
return
|
||||
|
||||
now = datetime.utcnow()
|
||||
current_power = battery.get("current_power_kw", 0)
|
||||
current_soc = battery.get("state_of_charge", 50)
|
||||
capacity = battery.get("capacity_kwh", 100)
|
||||
|
||||
# Simulate SOC changes based on power flow
|
||||
if current_power != 0:
|
||||
# Convert power to SOC change (simplified)
|
||||
soc_change = (current_power * 0.5) / capacity * 100 # 0.5 hour interval
|
||||
new_soc = max(0, min(100, current_soc + soc_change))
|
||||
|
||||
# Calculate stored energy
|
||||
stored_energy = new_soc / 100 * capacity
|
||||
|
||||
# Update database
|
||||
await self.batteries_collection.update_one(
|
||||
{"battery_id": battery_id},
|
||||
{
|
||||
"$set": {
|
||||
"state_of_charge": round(new_soc, 2),
|
||||
"stored_energy_kwh": round(stored_energy, 2),
|
||||
"last_updated": now
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Log historical data
|
||||
await self.battery_history_collection.insert_one({
|
||||
"battery_id": battery_id,
|
||||
"timestamp": now,
|
||||
"state_of_charge": new_soc,
|
||||
"power_kw": current_power,
|
||||
"stored_energy_kwh": stored_energy,
|
||||
"efficiency": battery.get("efficiency", 0.95)
|
||||
})
|
||||
|
||||
# Clear cache
|
||||
await self.redis.delete(f"battery:status:{battery_id}")
|
||||
|
||||
async def check_maintenance_alerts(self):
|
||||
"""Check for batteries needing maintenance"""
|
||||
batteries = await self.get_batteries()
|
||||
|
||||
for battery in batteries:
|
||||
alerts = []
|
||||
|
||||
# Check health
|
||||
health = battery.get("health_percentage", 100)
|
||||
if health < 70:
|
||||
alerts.append({
|
||||
"alert_type": "health",
|
||||
"severity": "critical",
|
||||
"message": f"Battery health is critically low at {health}%",
|
||||
"recommended_action": "Schedule immediate maintenance and consider replacement"
|
||||
})
|
||||
elif health < 85:
|
||||
alerts.append({
|
||||
"alert_type": "health",
|
||||
"severity": "warning",
|
||||
"message": f"Battery health is declining at {health}%",
|
||||
"recommended_action": "Schedule maintenance inspection"
|
||||
})
|
||||
|
||||
# Check cycles
|
||||
cycles = battery.get("cycles_completed", 0)
|
||||
max_cycles = battery.get("max_cycles", 5000)
|
||||
if cycles > max_cycles * 0.9:
|
||||
alerts.append({
|
||||
"alert_type": "cycles",
|
||||
"severity": "warning",
|
||||
"message": f"Battery has completed {cycles}/{max_cycles} cycles",
|
||||
"recommended_action": "Plan for battery replacement"
|
||||
})
|
||||
|
||||
# Check scheduled maintenance
|
||||
next_maintenance = battery.get("next_maintenance")
|
||||
if next_maintenance and datetime.fromisoformat(next_maintenance.replace('Z', '+00:00')) < datetime.utcnow():
|
||||
alerts.append({
|
||||
"alert_type": "scheduled",
|
||||
"severity": "info",
|
||||
"message": "Scheduled maintenance is due",
|
||||
"recommended_action": "Perform scheduled maintenance procedures"
|
||||
})
|
||||
|
||||
# Save alerts to database
|
||||
for alert in alerts:
|
||||
alert_doc = {
|
||||
"battery_id": battery["battery_id"],
|
||||
"timestamp": datetime.utcnow(),
|
||||
**alert
|
||||
}
|
||||
|
||||
# Check if alert already exists to avoid duplicates
|
||||
existing = await self.maintenance_alerts_collection.find_one({
|
||||
"battery_id": battery["battery_id"],
|
||||
"alert_type": alert["alert_type"],
|
||||
"severity": alert["severity"]
|
||||
})
|
||||
|
||||
if not existing:
|
||||
await self.maintenance_alerts_collection.insert_one(alert_doc)
|
||||
|
||||
async def _log_battery_event(self, battery_id: str, event_type: str, data: Dict[str, Any]):
|
||||
"""Log battery events for auditing"""
|
||||
event_doc = {
|
||||
"battery_id": battery_id,
|
||||
"event_type": event_type,
|
||||
"timestamp": datetime.utcnow(),
|
||||
"data": data
|
||||
}
|
||||
|
||||
await self.db.battery_events.insert_one(event_doc)
|
||||
104
microservices/battery-service/database.py
Normal file
104
microservices/battery-service/database.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Database connections for Battery Service
|
||||
"""
|
||||
|
||||
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
||||
import redis.asyncio as redis
|
||||
import logging
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Database configuration
|
||||
MONGO_URL = os.getenv("MONGO_URL", "mongodb://localhost:27017")
|
||||
DATABASE_NAME = os.getenv("DATABASE_NAME", "energy_dashboard_batteries")
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
|
||||
|
||||
# Global database clients
|
||||
_mongo_client: AsyncIOMotorClient = None
|
||||
_database: AsyncIOMotorDatabase = None
|
||||
_redis_client: redis.Redis = None
|
||||
|
||||
async def connect_to_mongo():
|
||||
"""Create MongoDB connection"""
|
||||
global _mongo_client, _database
|
||||
|
||||
try:
|
||||
_mongo_client = AsyncIOMotorClient(MONGO_URL)
|
||||
_database = _mongo_client[DATABASE_NAME]
|
||||
|
||||
# Test connection
|
||||
await _database.command("ping")
|
||||
logger.info(f"Connected to MongoDB: {DATABASE_NAME}")
|
||||
|
||||
# Create indexes
|
||||
await create_indexes()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to MongoDB: {e}")
|
||||
raise
|
||||
|
||||
async def connect_to_redis():
|
||||
"""Create Redis connection"""
|
||||
global _redis_client
|
||||
|
||||
try:
|
||||
_redis_client = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
await _redis_client.ping()
|
||||
logger.info("Connected to Redis")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Redis: {e}")
|
||||
raise
|
||||
|
||||
async def close_mongo_connection():
|
||||
"""Close MongoDB connection"""
|
||||
global _mongo_client
|
||||
|
||||
if _mongo_client:
|
||||
_mongo_client.close()
|
||||
logger.info("Disconnected from MongoDB")
|
||||
|
||||
async def get_database() -> AsyncIOMotorDatabase:
|
||||
"""Get database instance"""
|
||||
global _database
|
||||
|
||||
if _database is None:
|
||||
raise RuntimeError("Database not initialized. Call connect_to_mongo() first.")
|
||||
|
||||
return _database
|
||||
|
||||
async def get_redis() -> redis.Redis:
|
||||
"""Get Redis instance"""
|
||||
global _redis_client
|
||||
|
||||
if _redis_client is None:
|
||||
raise RuntimeError("Redis not initialized. Call connect_to_redis() first.")
|
||||
|
||||
return _redis_client
|
||||
|
||||
async def create_indexes():
|
||||
"""Create database indexes for performance"""
|
||||
db = await get_database()
|
||||
|
||||
# Indexes for batteries collection
|
||||
await db.batteries.create_index("battery_id", unique=True)
|
||||
await db.batteries.create_index("state")
|
||||
await db.batteries.create_index("building")
|
||||
await db.batteries.create_index("room")
|
||||
await db.batteries.create_index("last_updated")
|
||||
|
||||
# Indexes for battery_history collection
|
||||
await db.battery_history.create_index([("battery_id", 1), ("timestamp", -1)])
|
||||
await db.battery_history.create_index("timestamp")
|
||||
|
||||
# Indexes for maintenance_alerts collection
|
||||
await db.maintenance_alerts.create_index([("battery_id", 1), ("alert_type", 1)])
|
||||
await db.maintenance_alerts.create_index("timestamp")
|
||||
await db.maintenance_alerts.create_index("severity")
|
||||
|
||||
# Indexes for battery_events collection
|
||||
await db.battery_events.create_index([("battery_id", 1), ("timestamp", -1)])
|
||||
await db.battery_events.create_index("event_type")
|
||||
|
||||
logger.info("Database indexes created")
|
||||
262
microservices/battery-service/main.py
Normal file
262
microservices/battery-service/main.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Battery Management Microservice
|
||||
Handles battery monitoring, charging, and energy storage optimization.
|
||||
Port: 8002
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from models import (
|
||||
BatteryStatus, BatteryCommand, BatteryResponse, BatteryListResponse,
|
||||
ChargingRequest, HistoricalDataRequest, HealthResponse
|
||||
)
|
||||
from database import connect_to_mongo, close_mongo_connection, get_database, connect_to_redis, get_redis
|
||||
from battery_service import BatteryService
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager"""
|
||||
logger.info("Battery Service starting up...")
|
||||
await connect_to_mongo()
|
||||
await connect_to_redis()
|
||||
|
||||
# Start background tasks
|
||||
asyncio.create_task(battery_monitoring_task())
|
||||
|
||||
logger.info("Battery Service startup complete")
|
||||
|
||||
yield
|
||||
|
||||
logger.info("Battery Service shutting down...")
|
||||
await close_mongo_connection()
|
||||
logger.info("Battery Service shutdown complete")
|
||||
|
||||
app = FastAPI(
|
||||
title="Battery Management Service",
|
||||
description="Energy storage monitoring and control microservice",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Dependencies
|
||||
async def get_db():
|
||||
return await get_database()
|
||||
|
||||
async def get_battery_service(db=Depends(get_db)):
|
||||
redis = await get_redis()
|
||||
return BatteryService(db, redis)
|
||||
|
||||
@app.get("/health", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
try:
|
||||
db = await get_database()
|
||||
await db.command("ping")
|
||||
|
||||
redis = await get_redis()
|
||||
await redis.ping()
|
||||
|
||||
return HealthResponse(
|
||||
service="battery-service",
|
||||
status="healthy",
|
||||
timestamp=datetime.utcnow(),
|
||||
version="1.0.0"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
raise HTTPException(status_code=503, detail="Service Unavailable")
|
||||
|
||||
@app.get("/batteries", response_model=BatteryListResponse)
|
||||
async def get_batteries(service: BatteryService = Depends(get_battery_service)):
|
||||
"""Get all registered batteries"""
|
||||
try:
|
||||
batteries = await service.get_batteries()
|
||||
return BatteryListResponse(
|
||||
batteries=batteries,
|
||||
count=len(batteries),
|
||||
total_capacity=sum(b.get("capacity", 0) for b in batteries),
|
||||
total_stored_energy=sum(b.get("stored_energy", 0) for b in batteries)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting batteries: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/batteries/{battery_id}", response_model=BatteryResponse)
|
||||
async def get_battery(battery_id: str, service: BatteryService = Depends(get_battery_service)):
|
||||
"""Get specific battery status"""
|
||||
try:
|
||||
battery = await service.get_battery_status(battery_id)
|
||||
if not battery:
|
||||
raise HTTPException(status_code=404, detail="Battery not found")
|
||||
|
||||
return BatteryResponse(
|
||||
battery_id=battery_id,
|
||||
status=battery
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting battery {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/batteries/{battery_id}/charge")
|
||||
async def charge_battery(
|
||||
battery_id: str,
|
||||
request: ChargingRequest,
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Charge a battery with specified power"""
|
||||
try:
|
||||
result = await service.charge_battery(battery_id, request.power_kw, request.duration_minutes)
|
||||
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=400, detail=result.get("error", "Charging failed"))
|
||||
|
||||
return {
|
||||
"message": "Charging initiated successfully",
|
||||
"battery_id": battery_id,
|
||||
"power_kw": request.power_kw,
|
||||
"duration_minutes": request.duration_minutes,
|
||||
"estimated_completion": result.get("estimated_completion")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error charging battery {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/batteries/{battery_id}/discharge")
|
||||
async def discharge_battery(
|
||||
battery_id: str,
|
||||
request: ChargingRequest,
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Discharge a battery with specified power"""
|
||||
try:
|
||||
result = await service.discharge_battery(battery_id, request.power_kw, request.duration_minutes)
|
||||
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=400, detail=result.get("error", "Discharging failed"))
|
||||
|
||||
return {
|
||||
"message": "Discharging initiated successfully",
|
||||
"battery_id": battery_id,
|
||||
"power_kw": request.power_kw,
|
||||
"duration_minutes": request.duration_minutes,
|
||||
"estimated_completion": result.get("estimated_completion")
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error discharging battery {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/batteries/{battery_id}/history")
|
||||
async def get_battery_history(
|
||||
battery_id: str,
|
||||
hours: int = 24,
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Get battery historical data"""
|
||||
try:
|
||||
history = await service.get_battery_history(battery_id, hours)
|
||||
|
||||
return {
|
||||
"battery_id": battery_id,
|
||||
"period_hours": hours,
|
||||
"history": history,
|
||||
"data_points": len(history)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting battery history for {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.get("/batteries/analytics/summary")
|
||||
async def get_battery_analytics(
|
||||
hours: int = 24,
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Get battery system analytics"""
|
||||
try:
|
||||
analytics = await service.get_battery_analytics(hours)
|
||||
|
||||
return {
|
||||
"period_hours": hours,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
**analytics
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting battery analytics: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@app.post("/batteries/{battery_id}/optimize")
|
||||
async def optimize_battery(
|
||||
battery_id: str,
|
||||
target_soc: float, # State of Charge target (0-100%)
|
||||
service: BatteryService = Depends(get_battery_service)
|
||||
):
|
||||
"""Optimize battery charging/discharging to reach target SOC"""
|
||||
try:
|
||||
if not (0 <= target_soc <= 100):
|
||||
raise HTTPException(status_code=400, detail="Target SOC must be between 0 and 100")
|
||||
|
||||
result = await service.optimize_battery(battery_id, target_soc)
|
||||
|
||||
return {
|
||||
"battery_id": battery_id,
|
||||
"target_soc": target_soc,
|
||||
"optimization_plan": result,
|
||||
"message": "Battery optimization initiated"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing battery {battery_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def battery_monitoring_task():
|
||||
"""Background task for continuous battery monitoring"""
|
||||
logger.info("Starting battery monitoring task")
|
||||
|
||||
while True:
|
||||
try:
|
||||
db = await get_database()
|
||||
redis = await get_redis()
|
||||
service = BatteryService(db, redis)
|
||||
|
||||
# Update all battery statuses
|
||||
batteries = await service.get_batteries()
|
||||
for battery in batteries:
|
||||
await service.update_battery_status(battery["battery_id"])
|
||||
|
||||
# Check for maintenance alerts
|
||||
await service.check_maintenance_alerts()
|
||||
|
||||
# Sleep for monitoring interval (30 seconds)
|
||||
await asyncio.sleep(30)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in battery monitoring task: {e}")
|
||||
await asyncio.sleep(60) # Wait longer on error
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||
157
microservices/battery-service/models.py
Normal file
157
microservices/battery-service/models.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Pydantic models for Battery Management Service
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any, Literal
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
class BatteryState(str, Enum):
|
||||
IDLE = "idle"
|
||||
CHARGING = "charging"
|
||||
DISCHARGING = "discharging"
|
||||
MAINTENANCE = "maintenance"
|
||||
ERROR = "error"
|
||||
|
||||
class BatteryType(str, Enum):
|
||||
LITHIUM_ION = "lithium_ion"
|
||||
LEAD_ACID = "lead_acid"
|
||||
NICKEL_METAL_HYDRIDE = "nickel_metal_hydride"
|
||||
FLOW_BATTERY = "flow_battery"
|
||||
|
||||
class BatteryStatus(BaseModel):
|
||||
"""Battery status model"""
|
||||
battery_id: str = Field(..., description="Unique battery identifier")
|
||||
name: str = Field(..., description="Human-readable battery name")
|
||||
type: BatteryType = Field(..., description="Battery technology type")
|
||||
state: BatteryState = Field(..., description="Current operational state")
|
||||
|
||||
# Energy metrics
|
||||
capacity_kwh: float = Field(..., description="Total battery capacity in kWh")
|
||||
stored_energy_kwh: float = Field(..., description="Currently stored energy in kWh")
|
||||
state_of_charge: float = Field(..., description="State of charge (0-100%)")
|
||||
|
||||
# Power metrics
|
||||
max_charge_power_kw: float = Field(..., description="Maximum charging power in kW")
|
||||
max_discharge_power_kw: float = Field(..., description="Maximum discharging power in kW")
|
||||
current_power_kw: float = Field(0, description="Current power flow in kW (positive = charging)")
|
||||
|
||||
# Technical specifications
|
||||
efficiency: float = Field(0.95, description="Round-trip efficiency (0-1)")
|
||||
cycles_completed: int = Field(0, description="Number of charge/discharge cycles")
|
||||
max_cycles: int = Field(5000, description="Maximum rated cycles")
|
||||
|
||||
# Health and maintenance
|
||||
health_percentage: float = Field(100, description="Battery health (0-100%)")
|
||||
temperature_celsius: Optional[float] = Field(None, description="Battery temperature")
|
||||
last_maintenance: Optional[datetime] = Field(None, description="Last maintenance date")
|
||||
next_maintenance: Optional[datetime] = Field(None, description="Next maintenance date")
|
||||
|
||||
# Location and installation
|
||||
location: Optional[str] = Field(None, description="Physical location")
|
||||
building: Optional[str] = Field(None, description="Building identifier")
|
||||
room: Optional[str] = Field(None, description="Room identifier")
|
||||
|
||||
# Operational data
|
||||
installed_date: Optional[datetime] = Field(None, description="Installation date")
|
||||
last_updated: datetime = Field(default_factory=datetime.utcnow, description="Last status update")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None
|
||||
}
|
||||
|
||||
class BatteryCommand(BaseModel):
|
||||
"""Battery control command"""
|
||||
battery_id: str = Field(..., description="Target battery ID")
|
||||
command: Literal["charge", "discharge", "stop"] = Field(..., description="Command type")
|
||||
power_kw: Optional[float] = Field(None, description="Power level in kW")
|
||||
duration_minutes: Optional[int] = Field(None, description="Command duration in minutes")
|
||||
target_soc: Optional[float] = Field(None, description="Target state of charge (0-100%)")
|
||||
|
||||
class ChargingRequest(BaseModel):
|
||||
"""Battery charging/discharging request"""
|
||||
power_kw: float = Field(..., description="Power level in kW", gt=0)
|
||||
duration_minutes: Optional[int] = Field(None, description="Duration in minutes", gt=0)
|
||||
target_soc: Optional[float] = Field(None, description="Target SOC (0-100%)", ge=0, le=100)
|
||||
|
||||
class BatteryResponse(BaseModel):
|
||||
"""Battery operation response"""
|
||||
battery_id: str
|
||||
status: Dict[str, Any]
|
||||
message: Optional[str] = None
|
||||
|
||||
class BatteryListResponse(BaseModel):
|
||||
"""Response for battery list endpoint"""
|
||||
batteries: List[Dict[str, Any]]
|
||||
count: int
|
||||
total_capacity: float = Field(description="Total system capacity in kWh")
|
||||
total_stored_energy: float = Field(description="Total stored energy in kWh")
|
||||
|
||||
class HistoricalDataRequest(BaseModel):
|
||||
"""Request for historical battery data"""
|
||||
battery_id: str
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
hours: int = Field(default=24, description="Hours of data to retrieve")
|
||||
|
||||
class BatteryHistoricalData(BaseModel):
|
||||
"""Historical battery data point"""
|
||||
timestamp: datetime
|
||||
state_of_charge: float
|
||||
power_kw: float
|
||||
temperature_celsius: Optional[float] = None
|
||||
efficiency: float
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
|
||||
class BatteryAnalytics(BaseModel):
|
||||
"""Battery system analytics"""
|
||||
total_batteries: int
|
||||
active_batteries: int
|
||||
total_capacity_kwh: float
|
||||
total_stored_energy_kwh: float
|
||||
average_soc: float
|
||||
|
||||
# Energy flows
|
||||
total_energy_charged_kwh: float
|
||||
total_energy_discharged_kwh: float
|
||||
net_energy_flow_kwh: float
|
||||
|
||||
# Efficiency metrics
|
||||
round_trip_efficiency: float
|
||||
capacity_utilization: float
|
||||
|
||||
# Health metrics
|
||||
average_health: float
|
||||
batteries_needing_maintenance: int
|
||||
|
||||
class MaintenanceAlert(BaseModel):
|
||||
"""Battery maintenance alert"""
|
||||
battery_id: str
|
||||
alert_type: Literal["scheduled", "health", "temperature", "cycles"]
|
||||
severity: Literal["info", "warning", "critical"]
|
||||
message: str
|
||||
recommended_action: str
|
||||
timestamp: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response"""
|
||||
service: str
|
||||
status: str
|
||||
timestamp: datetime
|
||||
version: str
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
7
microservices/battery-service/requirements.txt
Normal file
7
microservices/battery-service/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
pymongo
|
||||
motor
|
||||
redis
|
||||
python-dotenv
|
||||
pydantic
|
||||
Reference in New Issue
Block a user