203 lines
6.3 KiB
Python
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")
|