Files
sac4cps-backend/main.py
rafaeldpsilva a7a18e6295 first commit
2025-09-09 13:46:42 +01:00

203 lines
6.3 KiB
Python

import asyncio
import json
import redis.asyncio as redis
import time
import os
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from typing import List, Optional
import logging
from contextlib import asynccontextmanager
# Import our custom modules
from database import connect_to_mongo, close_mongo_connection, redis_manager, schedule_cleanup
from persistence import persistence_service
from models import DataQuery, DataResponse, HealthCheck
from api import router as api_router
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Application startup time for uptime calculation
app_start_time = time.time()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager"""
# Startup
logger.info("Application starting up...")
# Connect to databases
await connect_to_mongo()
await persistence_service.initialize()
# Start background tasks
asyncio.create_task(redis_subscriber())
asyncio.create_task(schedule_cleanup())
logger.info("Application startup complete")
yield
# Shutdown
logger.info("Application shutting down...")
await close_mongo_connection()
await redis_manager.disconnect()
logger.info("Application shutdown complete")
app = FastAPI(
title="Energy Monitoring Dashboard API",
description="Real-time energy monitoring and IoT sensor data management system",
version="1.0.0",
lifespan=lifespan
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Configure appropriately for production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router
app.include_router(api_router, prefix="/api/v1")
# In-memory store for active WebSocket connections
active_connections: List[WebSocket] = []
# Redis channel to subscribe to
REDIS_CHANNEL = "energy_data"
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""
WebSocket endpoint that connects a client, adds them to the active pool,
and removes them on disconnection.
"""
await websocket.accept()
active_connections.append(websocket)
logger.info(f"New client connected. Total clients: {len(active_connections)}")
try:
while True:
# Keep the connection alive
await websocket.receive_text()
except WebSocketDisconnect:
active_connections.remove(websocket)
logger.info(f"Client disconnected. Total clients: {len(active_connections)}")
async def redis_subscriber():
"""
Connects to Redis, subscribes to the specified channel, and broadcasts
messages to all active WebSocket clients. Also persists data to MongoDB.
"""
logger.info("Starting Redis subscriber...")
try:
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
await r.ping()
logger.info("Successfully connected to Redis for subscription.")
except Exception as e:
logger.error(f"Could not connect to Redis for subscription: {e}")
return
pubsub = r.pubsub()
await pubsub.subscribe(REDIS_CHANNEL)
logger.info(f"Subscribed to Redis channel: '{REDIS_CHANNEL}'")
while True:
try:
message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=1.0)
if message:
message_data = message['data']
logger.debug(f"Received from Redis: {message_data}")
# Process and persist the data
await persistence_service.process_sensor_message(message_data)
# Broadcast message to all connected WebSocket clients
if active_connections:
await asyncio.gather(
*[connection.send_text(message_data) for connection in active_connections],
return_exceptions=True
)
except Exception as e:
logger.error(f"Error in Redis subscriber loop: {e}")
# Add a delay to prevent rapid-fire errors
await asyncio.sleep(5)
@app.get("/")
async def read_root():
"""Root endpoint with basic system information"""
return {
"message": "Energy Monitoring Dashboard Backend",
"version": "1.0.0",
"status": "running",
"uptime_seconds": time.time() - app_start_time
}
@app.get("/health", response_model=HealthCheck)
async def health_check():
"""Health check endpoint"""
try:
# Check database connections
mongodb_connected = True
redis_connected = True
try:
await persistence_service.db.command("ping")
except:
mongodb_connected = False
try:
await redis_manager.redis_client.ping()
except:
redis_connected = False
# Get system statistics
stats = await persistence_service.get_sensor_statistics()
# Determine overall status
status = "healthy"
if not mongodb_connected or not redis_connected:
status = "degraded"
return HealthCheck(
status=status,
mongodb_connected=mongodb_connected,
redis_connected=redis_connected,
total_sensors=stats.get("total_sensors", 0),
active_sensors=stats.get("active_sensors", 0),
total_readings=stats.get("total_readings", 0),
uptime_seconds=time.time() - app_start_time
)
except Exception as e:
logger.error(f"Health check failed: {e}")
raise HTTPException(status_code=503, detail="Service Unavailable")
@app.get("/status")
async def system_status():
"""Detailed system status endpoint"""
try:
stats = await persistence_service.get_sensor_statistics()
return {
"timestamp": time.time(),
"uptime_seconds": time.time() - app_start_time,
"active_websocket_connections": len(active_connections),
"database_stats": stats
}
except Exception as e:
logger.error(f"Status check failed: {e}")
raise HTTPException(status_code=500, detail="Internal Server Error")