"""Sensors module API routes.""" import logging from datetime import datetime from fastapi import APIRouter, HTTPException, Depends, WebSocket, WebSocketDisconnect, Query, BackgroundTasks from typing import Optional from .models import ( SensorReading, SensorMetadata, RoomCreate, RoomUpdate, DataQuery, DataResponse, SensorType, SensorStatus, HealthResponse ) from .sensor_service import SensorService from .room_service import RoomService from .analytics_service import AnalyticsService from .websocket_manager import WebSocketManager from core.dependencies import get_sensors_db, get_redis logger = logging.getLogger(__name__) # Create router router = APIRouter() # WebSocket manager (shared across all route handlers) websocket_manager = WebSocketManager() # Dependency functions async def get_sensor_service(db=Depends(get_sensors_db), redis=Depends(get_redis)): return SensorService(db, redis) async def get_room_service(db=Depends(get_sensors_db), redis=Depends(get_redis)): return RoomService(db, redis) async def get_analytics_service(db=Depends(get_sensors_db), redis=Depends(get_redis)): return AnalyticsService(db, redis) # Health check @router.get("/health", response_model=HealthResponse) async def health_check(db=Depends(get_sensors_db)): """Health check endpoint for sensors module""" try: await db.command("ping") return HealthResponse( service="sensors-module", 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 @router.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): """WebSocket endpoint for real-time sensor data""" await websocket_manager.connect(websocket) try: while True: await websocket.receive_text() except WebSocketDisconnect: await websocket_manager.disconnect(websocket) # Sensor Management Routes @router.get("/sensors/get") 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") @router.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") @router.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") @router.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") @router.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") @router.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 Routes @router.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") @router.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") @router.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") @router.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") @router.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") @router.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") @router.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 Routes @router.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") @router.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") @router.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 @router.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_result = await service.export_data( start_time=start_time, end_time=end_time, sensor_ids=sensor_ids, format=format ) return export_data_result except Exception as e: logger.error(f"Error exporting data: {e}") raise HTTPException(status_code=500, detail="Internal server error") # System Events @router.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 @router.post("/data/ingest") async def ingest_sensor_data( sensor_data: SensorReading, background_tasks: BackgroundTasks, service: SensorService = Depends(get_sensor_service), room_service: RoomService = Depends(get_room_service) ): """Ingest real-time sensor data""" try: result = await service.ingest_sensor_data(sensor_data) # Schedule background tasks if sensor_data.room: background_tasks.add_task(_update_room_metrics, room_service, 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 helper functions async def _update_room_metrics(room_service: RoomService, sensor_data: SensorReading): """Update room-level metrics when sensor data is received""" try: 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}")