- Implement RoomService for room management and metrics - Add AnalyticsService for sensor data analytics and trends - Extend models with Room, RoomCreate, RoomUpdate, RoomInfo - Add room CRUD endpoints to FastAPI app - Add database connection logic for MongoDB and Redis - Refactor sensor service logic into SensorService class
586 lines
20 KiB
Python
586 lines
20 KiB
Python
"""
|
|
Sensor Management Microservice
|
|
Handles sensors, rooms, real-time data, and analytics from the original dashboard.
|
|
Integrates all existing functionality with the microservices architecture.
|
|
Port: 8007
|
|
"""
|
|
|
|
import asyncio
|
|
from datetime import datetime, timedelta
|
|
from fastapi import FastAPI, HTTPException, Depends, WebSocket, WebSocketDisconnect, Query, BackgroundTasks
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from contextlib import asynccontextmanager
|
|
import logging
|
|
from typing import List, Optional, Dict, Any
|
|
import json
|
|
|
|
from models import (
|
|
SensorReading, SensorMetadata, RoomMetrics, SystemEvent, DataQuery, DataResponse,
|
|
SensorType, SensorStatus, CO2Status, OccupancyLevel, HealthResponse,
|
|
Room, RoomCreate, RoomUpdate, RoomInfo
|
|
)
|
|
from database import connect_to_mongo, close_mongo_connection, get_database, connect_to_redis, get_redis
|
|
from sensor_service import SensorService
|
|
from room_service import RoomService
|
|
from analytics_service import AnalyticsService
|
|
from websocket_manager import WebSocketManager
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# WebSocket manager for real-time updates
|
|
websocket_manager = WebSocketManager()
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Application lifespan manager"""
|
|
logger.info("Sensor Service starting up...")
|
|
await connect_to_mongo()
|
|
await connect_to_redis()
|
|
|
|
# Initialize default rooms if none exist
|
|
db = await get_database()
|
|
redis_client = await get_redis()
|
|
room_service = RoomService(db, redis_client)
|
|
await room_service.initialize_default_rooms()
|
|
|
|
# Start background tasks
|
|
asyncio.create_task(redis_subscriber_task())
|
|
asyncio.create_task(room_metrics_aggregation_task())
|
|
asyncio.create_task(data_cleanup_task())
|
|
|
|
logger.info("Sensor Service startup complete")
|
|
|
|
yield
|
|
|
|
logger.info("Sensor Service shutting down...")
|
|
await close_mongo_connection()
|
|
logger.info("Sensor Service shutdown complete")
|
|
|
|
app = FastAPI(
|
|
title="Sensor Management Service",
|
|
description="Comprehensive sensor, room, and analytics management 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_sensor_service(db=Depends(get_db)):
|
|
redis = await get_redis()
|
|
return SensorService(db, redis)
|
|
|
|
async def get_room_service(db=Depends(get_db)):
|
|
redis = await get_redis()
|
|
return RoomService(db, redis)
|
|
|
|
async def get_analytics_service(db=Depends(get_db)):
|
|
redis = await get_redis()
|
|
return AnalyticsService(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="sensor-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")
|
|
|
|
# WebSocket endpoint for real-time data
|
|
@app.websocket("/ws")
|
|
async def websocket_endpoint(websocket: WebSocket):
|
|
"""WebSocket endpoint for real-time sensor data"""
|
|
await websocket_manager.connect(websocket)
|
|
try:
|
|
while True:
|
|
# Keep connection alive and handle any incoming messages
|
|
await websocket.receive_text()
|
|
except WebSocketDisconnect:
|
|
await websocket_manager.disconnect(websocket)
|
|
|
|
# Original Dashboard API Endpoints
|
|
|
|
# Sensor Management
|
|
@app.get("/sensors")
|
|
async def get_sensors(
|
|
room: Optional[str] = Query(None, description="Filter by room"),
|
|
sensor_type: Optional[SensorType] = Query(None, description="Filter by sensor type"),
|
|
status: Optional[SensorStatus] = Query(None, description="Filter by status"),
|
|
service: SensorService = Depends(get_sensor_service)
|
|
):
|
|
"""Get all sensors with optional filtering"""
|
|
try:
|
|
sensors = await service.get_sensors(room=room, sensor_type=sensor_type, status=status)
|
|
return {
|
|
"sensors": sensors,
|
|
"count": len(sensors),
|
|
"filters": {
|
|
"room": room,
|
|
"sensor_type": sensor_type.value if sensor_type else None,
|
|
"status": status.value if status else None
|
|
}
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error getting sensors: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.get("/sensors/{sensor_id}")
|
|
async def get_sensor(sensor_id: str, service: SensorService = Depends(get_sensor_service)):
|
|
"""Get detailed sensor information"""
|
|
try:
|
|
sensor = await service.get_sensor_details(sensor_id)
|
|
if not sensor:
|
|
raise HTTPException(status_code=404, detail="Sensor not found")
|
|
|
|
return sensor
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting sensor {sensor_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.get("/sensors/{sensor_id}/data")
|
|
async def get_sensor_data(
|
|
sensor_id: str,
|
|
start_time: Optional[int] = Query(None, description="Start timestamp (Unix)"),
|
|
end_time: Optional[int] = Query(None, description="End timestamp (Unix)"),
|
|
limit: int = Query(100, description="Maximum records to return"),
|
|
offset: int = Query(0, description="Records to skip"),
|
|
service: SensorService = Depends(get_sensor_service)
|
|
):
|
|
"""Get historical data for a specific sensor"""
|
|
try:
|
|
data = await service.get_sensor_data(
|
|
sensor_id=sensor_id,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
limit=limit,
|
|
offset=offset
|
|
)
|
|
|
|
return DataResponse(
|
|
data=data["readings"],
|
|
total_count=data["total_count"],
|
|
query=DataQuery(
|
|
sensor_ids=[sensor_id],
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
limit=limit,
|
|
offset=offset
|
|
),
|
|
execution_time_ms=data.get("execution_time_ms", 0)
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error getting sensor data for {sensor_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.post("/sensors")
|
|
async def create_sensor(
|
|
sensor_data: SensorMetadata,
|
|
service: SensorService = Depends(get_sensor_service)
|
|
):
|
|
"""Register a new sensor"""
|
|
try:
|
|
result = await service.create_sensor(sensor_data)
|
|
return {
|
|
"message": "Sensor created successfully",
|
|
"sensor_id": sensor_data.sensor_id,
|
|
"created_at": result.get("created_at")
|
|
}
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error creating sensor: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.put("/sensors/{sensor_id}")
|
|
async def update_sensor(
|
|
sensor_id: str,
|
|
update_data: dict,
|
|
service: SensorService = Depends(get_sensor_service)
|
|
):
|
|
"""Update sensor metadata"""
|
|
try:
|
|
result = await service.update_sensor(sensor_id, update_data)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Sensor not found")
|
|
|
|
return {
|
|
"message": "Sensor updated successfully",
|
|
"sensor_id": sensor_id,
|
|
"updated_at": datetime.utcnow().isoformat()
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error updating sensor {sensor_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.delete("/sensors/{sensor_id}")
|
|
async def delete_sensor(
|
|
sensor_id: str,
|
|
service: SensorService = Depends(get_sensor_service)
|
|
):
|
|
"""Delete a sensor and all its data"""
|
|
try:
|
|
result = await service.delete_sensor(sensor_id)
|
|
return {
|
|
"message": "Sensor deleted successfully",
|
|
"sensor_id": sensor_id,
|
|
"readings_deleted": result.get("readings_deleted", 0)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error deleting sensor {sensor_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
# Room Management
|
|
@app.get("/rooms/names")
|
|
async def get_room_names(service: RoomService = Depends(get_room_service)):
|
|
"""Get simple list of room names for dropdowns"""
|
|
try:
|
|
room_names = await service.get_all_room_names()
|
|
return {
|
|
"rooms": room_names,
|
|
"count": len(room_names)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error getting room names: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.get("/rooms")
|
|
async def get_rooms(service: RoomService = Depends(get_room_service)):
|
|
"""Get all rooms with sensor counts and metrics"""
|
|
try:
|
|
rooms = await service.get_rooms()
|
|
return {
|
|
"rooms": rooms,
|
|
"count": len(rooms)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error getting rooms: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.post("/rooms")
|
|
async def create_room(
|
|
room_data: RoomCreate,
|
|
service: RoomService = Depends(get_room_service)
|
|
):
|
|
"""Create a new room"""
|
|
try:
|
|
result = await service.create_room(room_data.dict())
|
|
return {
|
|
"message": "Room created successfully",
|
|
"room": result["name"],
|
|
"created_at": result["created_at"]
|
|
}
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error creating room: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.put("/rooms/{room_name}")
|
|
async def update_room(
|
|
room_name: str,
|
|
room_data: RoomUpdate,
|
|
service: RoomService = Depends(get_room_service)
|
|
):
|
|
"""Update an existing room"""
|
|
try:
|
|
result = await service.update_room(room_name, room_data.dict(exclude_unset=True))
|
|
return {
|
|
"message": "Room updated successfully",
|
|
"room": result["name"],
|
|
"updated_at": result["updated_at"],
|
|
"modified": result["modified"]
|
|
}
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"Error updating room {room_name}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.delete("/rooms/{room_name}")
|
|
async def delete_room(room_name: str, service: RoomService = Depends(get_room_service)):
|
|
"""Delete a room"""
|
|
try:
|
|
result = await service.delete_room(room_name)
|
|
return {
|
|
"message": "Room deleted successfully",
|
|
**result
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error deleting room {room_name}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.get("/rooms/{room_name}")
|
|
async def get_room(room_name: str, service: RoomService = Depends(get_room_service)):
|
|
"""Get detailed room information"""
|
|
try:
|
|
room = await service.get_room_details(room_name)
|
|
if not room:
|
|
raise HTTPException(status_code=404, detail="Room not found")
|
|
|
|
return room
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting room {room_name}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.get("/rooms/{room_name}/data")
|
|
async def get_room_data(
|
|
room_name: str,
|
|
start_time: Optional[int] = Query(None, description="Start timestamp (Unix)"),
|
|
end_time: Optional[int] = Query(None, description="End timestamp (Unix)"),
|
|
limit: int = Query(100, description="Maximum records to return"),
|
|
service: RoomService = Depends(get_room_service)
|
|
):
|
|
"""Get historical data for a specific room"""
|
|
try:
|
|
data = await service.get_room_data(
|
|
room_name=room_name,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
limit=limit
|
|
)
|
|
|
|
return {
|
|
"room": room_name,
|
|
"room_metrics": data.get("room_metrics", []),
|
|
"sensor_readings": data.get("sensor_readings", []),
|
|
"period": {
|
|
"start_time": start_time,
|
|
"end_time": end_time
|
|
}
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error getting room data for {room_name}: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
# Analytics
|
|
@app.post("/data/query")
|
|
async def query_data(
|
|
query_params: DataQuery,
|
|
service: AnalyticsService = Depends(get_analytics_service)
|
|
):
|
|
"""Advanced data querying with multiple filters"""
|
|
try:
|
|
result = await service.query_data(query_params)
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error executing data query: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.get("/analytics/summary")
|
|
async def get_analytics_summary(
|
|
hours: int = Query(24, description="Hours of data to analyze"),
|
|
service: AnalyticsService = Depends(get_analytics_service)
|
|
):
|
|
"""Get comprehensive analytics summary"""
|
|
try:
|
|
analytics = await service.get_analytics_summary(hours)
|
|
return analytics
|
|
except Exception as e:
|
|
logger.error(f"Error getting analytics summary: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
@app.get("/analytics/energy")
|
|
async def get_energy_analytics(
|
|
hours: int = Query(24),
|
|
room: Optional[str] = Query(None),
|
|
service: AnalyticsService = Depends(get_analytics_service)
|
|
):
|
|
"""Get energy-specific analytics"""
|
|
try:
|
|
analytics = await service.get_energy_analytics(hours, room)
|
|
return analytics
|
|
except Exception as e:
|
|
logger.error(f"Error getting energy analytics: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
# Data Export
|
|
@app.get("/export")
|
|
async def export_data(
|
|
start_time: int = Query(..., description="Start timestamp (Unix)"),
|
|
end_time: int = Query(..., description="End timestamp (Unix)"),
|
|
sensor_ids: Optional[str] = Query(None, description="Comma-separated sensor IDs"),
|
|
format: str = Query("json", description="Export format (json, csv)"),
|
|
service: SensorService = Depends(get_sensor_service)
|
|
):
|
|
"""Export sensor data"""
|
|
try:
|
|
export_data = await service.export_data(
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
sensor_ids=sensor_ids,
|
|
format=format
|
|
)
|
|
|
|
return export_data
|
|
except Exception as e:
|
|
logger.error(f"Error exporting data: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
# System Events
|
|
@app.get("/events")
|
|
async def get_events(
|
|
severity: Optional[str] = Query(None, description="Filter by severity"),
|
|
event_type: Optional[str] = Query(None, description="Filter by event type"),
|
|
hours: int = Query(24, description="Hours of events to retrieve"),
|
|
limit: int = Query(50, description="Maximum events to return"),
|
|
service: SensorService = Depends(get_sensor_service)
|
|
):
|
|
"""Get system events and alerts"""
|
|
try:
|
|
events = await service.get_events(
|
|
severity=severity,
|
|
event_type=event_type,
|
|
hours=hours,
|
|
limit=limit
|
|
)
|
|
|
|
return {
|
|
"events": events,
|
|
"count": len(events),
|
|
"period_hours": hours
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error getting events: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
# Real-time data ingestion endpoint (for simulators)
|
|
@app.post("/data/ingest")
|
|
async def ingest_sensor_data(
|
|
sensor_data: SensorReading,
|
|
background_tasks: BackgroundTasks,
|
|
service: SensorService = Depends(get_sensor_service)
|
|
):
|
|
"""Ingest real-time sensor data"""
|
|
try:
|
|
# Process and store sensor data
|
|
result = await service.ingest_sensor_data(sensor_data)
|
|
|
|
# Schedule background tasks for analytics
|
|
background_tasks.add_task(update_room_metrics, sensor_data)
|
|
background_tasks.add_task(broadcast_sensor_data, sensor_data)
|
|
|
|
return {
|
|
"message": "Sensor data ingested successfully",
|
|
"sensor_id": sensor_data.sensor_id,
|
|
"timestamp": sensor_data.timestamp
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error ingesting sensor data: {e}")
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
# Background task functions
|
|
async def update_room_metrics(sensor_data: SensorReading):
|
|
"""Update room-level metrics when sensor data is received"""
|
|
if sensor_data.room:
|
|
try:
|
|
db = await get_database()
|
|
redis = await get_redis()
|
|
room_service = RoomService(db, redis)
|
|
await room_service.update_room_metrics(sensor_data)
|
|
except Exception as e:
|
|
logger.error(f"Error updating room metrics: {e}")
|
|
|
|
async def broadcast_sensor_data(sensor_data: SensorReading):
|
|
"""Broadcast sensor data to WebSocket clients"""
|
|
try:
|
|
await websocket_manager.broadcast_sensor_data(sensor_data)
|
|
except Exception as e:
|
|
logger.error(f"Error broadcasting sensor data: {e}")
|
|
|
|
# Background tasks
|
|
async def redis_subscriber_task():
|
|
"""Subscribe to Redis channels for real-time data"""
|
|
logger.info("Starting Redis subscriber task")
|
|
|
|
try:
|
|
redis = await get_redis()
|
|
pubsub = redis.pubsub()
|
|
await pubsub.subscribe("energy_data", "sensor_events")
|
|
|
|
while True:
|
|
try:
|
|
message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=1.0)
|
|
if message:
|
|
# Process incoming message and broadcast to WebSocket clients
|
|
await websocket_manager.broadcast_raw_data(message['data'])
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing Redis message: {e}")
|
|
await asyncio.sleep(5)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Redis subscriber task failed: {e}")
|
|
|
|
async def room_metrics_aggregation_task():
|
|
"""Periodically aggregate room-level metrics"""
|
|
logger.info("Starting room metrics aggregation task")
|
|
|
|
while True:
|
|
try:
|
|
db = await get_database()
|
|
redis = await get_redis()
|
|
room_service = RoomService(db, redis)
|
|
|
|
# Aggregate metrics for all rooms
|
|
await room_service.aggregate_all_room_metrics()
|
|
|
|
# Sleep for 5 minutes between aggregations
|
|
await asyncio.sleep(300)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in room metrics aggregation: {e}")
|
|
await asyncio.sleep(600) # Wait longer on error
|
|
|
|
async def data_cleanup_task():
|
|
"""Periodic cleanup of old data"""
|
|
logger.info("Starting data cleanup task")
|
|
|
|
while True:
|
|
try:
|
|
db = await get_database()
|
|
service = SensorService(db, None)
|
|
|
|
# Clean up data older than 90 days
|
|
cleanup_date = datetime.utcnow() - timedelta(days=90)
|
|
await service.cleanup_old_data(cleanup_date)
|
|
|
|
# Sleep for 24 hours between cleanups
|
|
await asyncio.sleep(86400)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in data cleanup task: {e}")
|
|
await asyncio.sleep(7200) # Wait 2 hours on error
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8007)
|