from fastapi import APIRouter, HTTPException, Query, Depends from typing import List, Optional, Dict, Any from datetime import datetime, timedelta import time import logging from pymongo import ASCENDING, DESCENDING from database import get_database, redis_manager from models import ( DataQuery, DataResponse, SensorReading, SensorMetadata, RoomMetrics, SystemEvent, SensorType, SensorStatus ) from persistence import persistence_service from services.token_service import TokenService logger = logging.getLogger(__name__) router = APIRouter() # Dependency to get database async def get_db(): return await get_database() @router.get("/sensors", summary="Get all 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"), db=Depends(get_db) ): """Get list of all registered sensors with optional filtering""" try: # Build query query = {} if room: query["room"] = room if sensor_type: query["sensor_type"] = sensor_type.value if status: query["status"] = status.value # Execute query cursor = db.sensor_metadata.find(query).sort("created_at", DESCENDING) sensors = await cursor.to_list(length=None) # Convert ObjectId to string for sensor in sensors: sensor["_id"] = str(sensor["_id"]) return { "sensors": sensors, "count": len(sensors), "query": query } except Exception as e: logger.error(f"Error getting sensors: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/sensors/{sensor_id}", summary="Get sensor details") async def get_sensor(sensor_id: str, db=Depends(get_db)): """Get detailed information about a specific sensor""" try: # Get sensor metadata sensor = await db.sensor_metadata.find_one({"sensor_id": sensor_id}) if not sensor: raise HTTPException(status_code=404, detail="Sensor not found") sensor["_id"] = str(sensor["_id"]) # Get recent readings (last 24 hours) recent_readings = await persistence_service.get_recent_readings( sensor_id=sensor_id, limit=100, minutes=1440 # 24 hours ) # Get latest reading from Redis latest_reading = await redis_manager.get_sensor_data(sensor_id) return { "sensor": sensor, "latest_reading": latest_reading, "recent_readings_count": len(recent_readings), "recent_readings": recent_readings[:10] # Return only 10 most recent } 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") @router.get("/sensors/{sensor_id}/data", summary="Get sensor historical 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"), db=Depends(get_db) ): """Get historical data for a specific sensor""" try: start_query_time = time.time() # Build time range query query = {"sensor_id": sensor_id} if start_time or end_time: time_query = {} if start_time: time_query["$gte"] = datetime.fromtimestamp(start_time) if end_time: time_query["$lte"] = datetime.fromtimestamp(end_time) query["created_at"] = time_query # Get total count total_count = await db.sensor_readings.count_documents(query) # Execute query with pagination cursor = db.sensor_readings.find(query).sort("timestamp", DESCENDING).skip(offset).limit(limit) readings = await cursor.to_list(length=limit) # Convert ObjectId to string for reading in readings: reading["_id"] = str(reading["_id"]) execution_time = (time.time() - start_query_time) * 1000 # Convert to milliseconds return DataResponse( data=readings, total_count=total_count, query=DataQuery( sensor_ids=[sensor_id], start_time=start_time, end_time=end_time, limit=limit, offset=offset ), execution_time_ms=execution_time ) except Exception as e: logger.error(f"Error getting sensor data for {sensor_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/rooms", summary="Get all rooms") async def get_rooms(db=Depends(get_db)): """Get list of all rooms with sensor counts""" try: # Get distinct rooms from sensor readings rooms = await db.sensor_readings.distinct("room", {"room": {"$ne": None}}) room_data = [] for room in rooms: # Get sensor count for each room sensor_count = len(await db.sensor_readings.distinct("sensor_id", {"room": room})) # Get latest room metrics from Redis room_metrics = await redis_manager.get_room_metrics(room) room_data.append({ "room": room, "sensor_count": sensor_count, "latest_metrics": room_metrics }) return { "rooms": room_data, "count": len(room_data) } except Exception as e: logger.error(f"Error getting rooms: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/rooms/{room_name}/data", summary="Get room historical 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"), db=Depends(get_db) ): """Get historical data for a specific room""" try: start_query_time = time.time() # Build query for room metrics query = {"room": room_name} if start_time or end_time: time_query = {} if start_time: time_query["$gte"] = datetime.fromtimestamp(start_time) if end_time: time_query["$lte"] = datetime.fromtimestamp(end_time) query["created_at"] = time_query # Get room metrics cursor = db.room_metrics.find(query).sort("timestamp", DESCENDING).limit(limit) room_metrics = await cursor.to_list(length=limit) # Also get sensor readings for the room sensor_query = {"room": room_name} if "created_at" in query: sensor_query["created_at"] = query["created_at"] sensor_cursor = db.sensor_readings.find(sensor_query).sort("timestamp", DESCENDING).limit(limit) sensor_readings = await sensor_cursor.to_list(length=limit) # Convert ObjectId to string for item in room_metrics + sensor_readings: item["_id"] = str(item["_id"]) execution_time = (time.time() - start_query_time) * 1000 return { "room": room_name, "room_metrics": room_metrics, "sensor_readings": sensor_readings, "execution_time_ms": execution_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") @router.post("/data/query", summary="Advanced data query", response_model=DataResponse) async def query_data(query_params: DataQuery, db=Depends(get_db)): """Advanced data querying with multiple filters and aggregations""" try: start_query_time = time.time() # Build MongoDB query mongo_query = {} # Sensor filters if query_params.sensor_ids: mongo_query["sensor_id"] = {"$in": query_params.sensor_ids} if query_params.rooms: mongo_query["room"] = {"$in": query_params.rooms} if query_params.sensor_types: mongo_query["sensor_type"] = {"$in": [st.value for st in query_params.sensor_types]} # Time range if query_params.start_time or query_params.end_time: time_query = {} if query_params.start_time: time_query["$gte"] = datetime.fromtimestamp(query_params.start_time) if query_params.end_time: time_query["$lte"] = datetime.fromtimestamp(query_params.end_time) mongo_query["created_at"] = time_query # Get total count total_count = await db.sensor_readings.count_documents(mongo_query) # Execute query with pagination and sorting sort_direction = DESCENDING if query_params.sort_order == "desc" else ASCENDING cursor = db.sensor_readings.find(mongo_query).sort( query_params.sort_by, sort_direction ).skip(query_params.offset).limit(query_params.limit) readings = await cursor.to_list(length=query_params.limit) # Convert ObjectId to string for reading in readings: reading["_id"] = str(reading["_id"]) execution_time = (time.time() - start_query_time) * 1000 return DataResponse( data=readings, total_count=total_count, query=query_params, execution_time_ms=execution_time ) except Exception as e: logger.error(f"Error executing data query: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/analytics/summary", summary="Get analytics summary") async def get_analytics_summary( hours: int = Query(24, description="Hours of data to analyze"), db=Depends(get_db) ): """Get analytics summary for the specified time period""" try: start_time = datetime.utcnow() - timedelta(hours=hours) # Aggregation pipeline for analytics pipeline = [ {"$match": {"created_at": {"$gte": start_time}}}, {"$group": { "_id": { "sensor_id": "$sensor_id", "room": "$room", "sensor_type": "$sensor_type" }, "reading_count": {"$sum": 1}, "avg_energy": {"$avg": "$energy.value"}, "total_energy": {"$sum": "$energy.value"}, "avg_co2": {"$avg": "$co2.value"}, "max_co2": {"$max": "$co2.value"}, "avg_temperature": {"$avg": "$temperature.value"}, "latest_timestamp": {"$max": "$timestamp"} }}, {"$sort": {"total_energy": -1}} ] cursor = db.sensor_readings.aggregate(pipeline) analytics = await cursor.to_list(length=None) # Room-level summary room_pipeline = [ {"$match": {"created_at": {"$gte": start_time}, "room": {"$ne": None}}}, {"$group": { "_id": "$room", "sensor_count": {"$addToSet": "$sensor_id"}, "total_energy": {"$sum": "$energy.value"}, "avg_co2": {"$avg": "$co2.value"}, "max_co2": {"$max": "$co2.value"}, "reading_count": {"$sum": 1} }}, {"$project": { "room": "$_id", "sensor_count": {"$size": "$sensor_count"}, "total_energy": 1, "avg_co2": 1, "max_co2": 1, "reading_count": 1 }}, {"$sort": {"total_energy": -1}} ] room_cursor = db.sensor_readings.aggregate(room_pipeline) room_analytics = await room_cursor.to_list(length=None) return { "period_hours": hours, "start_time": start_time.isoformat(), "sensor_analytics": analytics, "room_analytics": room_analytics, "summary": { "total_sensors_analyzed": len(analytics), "total_rooms_analyzed": len(room_analytics), "total_readings": sum(item["reading_count"] for item in analytics) } } except Exception as e: logger.error(f"Error getting analytics summary: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/events", summary="Get system 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"), db=Depends(get_db) ): """Get recent system events and alerts""" try: start_time = datetime.utcnow() - timedelta(hours=hours) # Build query query = {"created_at": {"$gte": start_time}} if severity: query["severity"] = severity if event_type: query["event_type"] = event_type # Execute query cursor = db.system_events.find(query).sort("timestamp", DESCENDING).limit(limit) events = await cursor.to_list(length=limit) # Convert ObjectId to string for event in events: event["_id"] = str(event["_id"]) 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") @router.put("/sensors/{sensor_id}/metadata", summary="Update sensor metadata") async def update_sensor_metadata( sensor_id: str, metadata: dict, db=Depends(get_db) ): """Update sensor metadata""" try: # Update timestamp metadata["updated_at"] = datetime.utcnow() result = await db.sensor_metadata.update_one( {"sensor_id": sensor_id}, {"$set": metadata} ) if result.matched_count == 0: raise HTTPException(status_code=404, detail="Sensor not found") return {"message": "Sensor metadata updated successfully", "modified": result.modified_count > 0} except HTTPException: raise except Exception as e: logger.error(f"Error updating sensor metadata: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.delete("/sensors/{sensor_id}", summary="Delete sensor and all its data") async def delete_sensor(sensor_id: str, db=Depends(get_db)): """Delete a sensor and all its associated data""" try: # Delete sensor readings readings_result = await db.sensor_readings.delete_many({"sensor_id": sensor_id}) # Delete sensor metadata metadata_result = await db.sensor_metadata.delete_one({"sensor_id": sensor_id}) # Delete from Redis cache await redis_manager.redis_client.delete(f"sensor:latest:{sensor_id}") await redis_manager.redis_client.delete(f"sensor:status:{sensor_id}") if metadata_result.deleted_count == 0: raise HTTPException(status_code=404, detail="Sensor not found") return { "message": "Sensor deleted successfully", "sensor_id": sensor_id, "readings_deleted": readings_result.deleted_count, "metadata_deleted": metadata_result.deleted_count } except HTTPException: raise except Exception as e: logger.error(f"Error deleting sensor {sensor_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/export", summary="Export data") 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)"), db=Depends(get_db) ): """Export sensor data for the specified time range""" try: # Build query query = { "created_at": { "$gte": datetime.fromtimestamp(start_time), "$lte": datetime.fromtimestamp(end_time) } } if sensor_ids: sensor_list = [sid.strip() for sid in sensor_ids.split(",")] query["sensor_id"] = {"$in": sensor_list} # Get data cursor = db.sensor_readings.find(query).sort("timestamp", ASCENDING) readings = await cursor.to_list(length=None) # Convert ObjectId to string for reading in readings: reading["_id"] = str(reading["_id"]) # Convert datetime to ISO string for JSON serialization if "created_at" in reading: reading["created_at"] = reading["created_at"].isoformat() if format.lower() == "csv": # TODO: Implement CSV export raise HTTPException(status_code=501, detail="CSV export not yet implemented") return { "data": readings, "count": len(readings), "export_params": { "start_time": start_time, "end_time": end_time, "sensor_ids": sensor_ids.split(",") if sensor_ids else None, "format": format } } except HTTPException: raise except Exception as e: logger.error(f"Error exporting data: {e}") raise HTTPException(status_code=500, detail="Internal server error") # Token Management Endpoints @router.get("/tokens", summary="Get all tokens") async def get_tokens(db=Depends(get_db)): """Get list of all tokens""" try: token_service = TokenService(db) tokens = await token_service.get_tokens() return {"tokens": tokens} except Exception as e: logger.error(f"Error getting tokens: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/tokens/generate", summary="Generate new token") async def generate_token( name: str, list_of_resources: List[str], data_aggregation: bool = False, time_aggregation: bool = False, embargo: int = 0, exp_hours: int = 24, db=Depends(get_db) ): """Generate a new JWT token with specified permissions""" try: token_service = TokenService(db) token = token_service.generate_token( name=name, list_of_resources=list_of_resources, data_aggregation=data_aggregation, time_aggregation=time_aggregation, embargo=embargo, exp_hours=exp_hours ) return {"token": token} except Exception as e: logger.error(f"Error generating token: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/tokens/check", summary="Validate token") async def check_token(token: str, db=Depends(get_db)): """Check token validity and decode payload""" try: token_service = TokenService(db) decoded = token_service.decode_token(token) return decoded except Exception as e: logger.error(f"Error checking token: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/tokens/save", summary="Save token to database") async def save_token(token: str, db=Depends(get_db)): """Save a valid token to the database""" try: token_service = TokenService(db) result = await token_service.insert_token(token) return result except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Error saving token: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/tokens/revoke", summary="Revoke token") async def revoke_token(token: str, db=Depends(get_db)): """Revoke a token by marking it as inactive""" try: token_service = TokenService(db) result = await token_service.revoke_token(token) return result except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Error revoking token: {e}") raise HTTPException(status_code=500, detail="Internal server error")