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