Remove legacy API and database modules for layered refactor
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
### Python ###
|
||||
#Claude file
|
||||
.claude/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
582
api.py
582
api.py
@@ -1,582 +0,0 @@
|
||||
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")
|
||||
220
database.py
220
database.py
@@ -1,220 +0,0 @@
|
||||
import os
|
||||
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
||||
from pymongo import IndexModel, ASCENDING, DESCENDING
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MongoDB:
|
||||
client: Optional[AsyncIOMotorClient] = None
|
||||
database: Optional[AsyncIOMotorDatabase] = None
|
||||
|
||||
# Global MongoDB instance
|
||||
mongodb = MongoDB()
|
||||
|
||||
async def connect_to_mongo():
|
||||
"""Create database connection"""
|
||||
try:
|
||||
# MongoDB connection string - default to localhost for development
|
||||
mongodb_url = os.getenv("MONGODB_URL", "mongodb://localhost:27017")
|
||||
database_name = os.getenv("DATABASE_NAME", "energy_monitoring")
|
||||
|
||||
logger.info(f"Connecting to MongoDB at: {mongodb_url}")
|
||||
|
||||
# Create async MongoDB client
|
||||
mongodb.client = AsyncIOMotorClient(mongodb_url)
|
||||
|
||||
# Test the connection
|
||||
await mongodb.client.admin.command('ping')
|
||||
logger.info("Successfully connected to MongoDB")
|
||||
|
||||
# Get database
|
||||
mongodb.database = mongodb.client[database_name]
|
||||
|
||||
# Create indexes for better performance
|
||||
await create_indexes()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting to MongoDB: {e}")
|
||||
raise
|
||||
|
||||
async def close_mongo_connection():
|
||||
"""Close database connection"""
|
||||
if mongodb.client:
|
||||
mongodb.client.close()
|
||||
logger.info("Disconnected from MongoDB")
|
||||
|
||||
async def create_indexes():
|
||||
"""Create database indexes for optimal performance"""
|
||||
try:
|
||||
# Sensor readings collection indexes
|
||||
sensor_readings_indexes = [
|
||||
IndexModel([("sensor_id", ASCENDING), ("timestamp", DESCENDING)]),
|
||||
IndexModel([("timestamp", DESCENDING)]),
|
||||
IndexModel([("room", ASCENDING), ("timestamp", DESCENDING)]),
|
||||
IndexModel([("sensor_type", ASCENDING), ("timestamp", DESCENDING)]),
|
||||
IndexModel([("created_at", DESCENDING)]),
|
||||
]
|
||||
await mongodb.database.sensor_readings.create_indexes(sensor_readings_indexes)
|
||||
|
||||
# Room metrics collection indexes
|
||||
room_metrics_indexes = [
|
||||
IndexModel([("room", ASCENDING), ("timestamp", DESCENDING)]),
|
||||
IndexModel([("timestamp", DESCENDING)]),
|
||||
IndexModel([("created_at", DESCENDING)]),
|
||||
]
|
||||
await mongodb.database.room_metrics.create_indexes(room_metrics_indexes)
|
||||
|
||||
# Sensor metadata collection indexes
|
||||
sensor_metadata_indexes = [
|
||||
IndexModel([("sensor_id", ASCENDING)], unique=True),
|
||||
IndexModel([("room", ASCENDING)]),
|
||||
IndexModel([("sensor_type", ASCENDING)]),
|
||||
IndexModel([("status", ASCENDING)]),
|
||||
]
|
||||
await mongodb.database.sensor_metadata.create_indexes(sensor_metadata_indexes)
|
||||
|
||||
# System events collection indexes
|
||||
system_events_indexes = [
|
||||
IndexModel([("timestamp", DESCENDING)]),
|
||||
IndexModel([("event_type", ASCENDING), ("timestamp", DESCENDING)]),
|
||||
IndexModel([("severity", ASCENDING), ("timestamp", DESCENDING)]),
|
||||
]
|
||||
await mongodb.database.system_events.create_indexes(system_events_indexes)
|
||||
|
||||
logger.info("Database indexes created successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating indexes: {e}")
|
||||
|
||||
async def get_database() -> AsyncIOMotorDatabase:
|
||||
"""Get database instance"""
|
||||
if not mongodb.database:
|
||||
await connect_to_mongo()
|
||||
return mongodb.database
|
||||
|
||||
class RedisManager:
|
||||
"""Redis connection and operations manager"""
|
||||
|
||||
def __init__(self):
|
||||
self.redis_client = None
|
||||
self.redis_host = os.getenv("REDIS_HOST", "localhost")
|
||||
self.redis_port = int(os.getenv("REDIS_PORT", "6379"))
|
||||
self.redis_db = int(os.getenv("REDIS_DB", "0"))
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to Redis"""
|
||||
try:
|
||||
import redis.asyncio as redis
|
||||
self.redis_client = redis.Redis(
|
||||
host=self.redis_host,
|
||||
port=self.redis_port,
|
||||
db=self.redis_db,
|
||||
decode_responses=True
|
||||
)
|
||||
await self.redis_client.ping()
|
||||
logger.info("Successfully connected to Redis")
|
||||
except Exception as e:
|
||||
logger.error(f"Error connecting to Redis: {e}")
|
||||
raise
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from Redis"""
|
||||
if self.redis_client:
|
||||
await self.redis_client.close()
|
||||
logger.info("Disconnected from Redis")
|
||||
|
||||
async def set_sensor_data(self, sensor_id: str, data: dict, expire_time: int = 3600):
|
||||
"""Store latest sensor data in Redis with expiration"""
|
||||
if not self.redis_client:
|
||||
await self.connect()
|
||||
|
||||
key = f"sensor:latest:{sensor_id}"
|
||||
await self.redis_client.setex(key, expire_time, str(data))
|
||||
|
||||
async def get_sensor_data(self, sensor_id: str) -> Optional[dict]:
|
||||
"""Get latest sensor data from Redis"""
|
||||
if not self.redis_client:
|
||||
await self.connect()
|
||||
|
||||
key = f"sensor:latest:{sensor_id}"
|
||||
data = await self.redis_client.get(key)
|
||||
if data:
|
||||
import json
|
||||
return json.loads(data)
|
||||
return None
|
||||
|
||||
async def set_room_metrics(self, room: str, metrics: dict, expire_time: int = 1800):
|
||||
"""Store room aggregated metrics in Redis"""
|
||||
if not self.redis_client:
|
||||
await self.connect()
|
||||
|
||||
key = f"room:metrics:{room}"
|
||||
await self.redis_client.setex(key, expire_time, str(metrics))
|
||||
|
||||
async def get_room_metrics(self, room: str) -> Optional[dict]:
|
||||
"""Get room aggregated metrics from Redis"""
|
||||
if not self.redis_client:
|
||||
await self.connect()
|
||||
|
||||
key = f"room:metrics:{room}"
|
||||
data = await self.redis_client.get(key)
|
||||
if data:
|
||||
import json
|
||||
return json.loads(data)
|
||||
return None
|
||||
|
||||
async def get_all_active_sensors(self) -> list:
|
||||
"""Get list of all sensors with recent data in Redis"""
|
||||
if not self.redis_client:
|
||||
await self.connect()
|
||||
|
||||
keys = await self.redis_client.keys("sensor:latest:*")
|
||||
return [key.replace("sensor:latest:", "") for key in keys]
|
||||
|
||||
# Global Redis manager instance
|
||||
redis_manager = RedisManager()
|
||||
|
||||
async def cleanup_old_data():
|
||||
"""Cleanup old data from MongoDB (retention policy)"""
|
||||
try:
|
||||
db = await get_database()
|
||||
|
||||
# Delete sensor readings older than 90 days
|
||||
retention_date = datetime.utcnow() - timedelta(days=90)
|
||||
result = await db.sensor_readings.delete_many({
|
||||
"created_at": {"$lt": retention_date}
|
||||
})
|
||||
|
||||
if result.deleted_count > 0:
|
||||
logger.info(f"Deleted {result.deleted_count} old sensor readings")
|
||||
|
||||
# Delete room metrics older than 30 days
|
||||
retention_date = datetime.utcnow() - timedelta(days=30)
|
||||
result = await db.room_metrics.delete_many({
|
||||
"created_at": {"$lt": retention_date}
|
||||
})
|
||||
|
||||
if result.deleted_count > 0:
|
||||
logger.info(f"Deleted {result.deleted_count} old room metrics")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up old data: {e}")
|
||||
|
||||
# Scheduled cleanup task
|
||||
async def schedule_cleanup():
|
||||
"""Schedule periodic cleanup of old data"""
|
||||
while True:
|
||||
try:
|
||||
await cleanup_old_data()
|
||||
# Wait 24 hours before next cleanup
|
||||
await asyncio.sleep(24 * 60 * 60)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in scheduled cleanup: {e}")
|
||||
# Wait 1 hour before retrying
|
||||
await asyncio.sleep(60 * 60)
|
||||
273
main_layered.py
273
main_layered.py
@@ -1,273 +0,0 @@
|
||||
"""
|
||||
Main application entry point with layered architecture
|
||||
This is the new structured version of the FastAPI application
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import logging
|
||||
|
||||
# Import layered components
|
||||
from layers.infrastructure.database_connection import database_connection
|
||||
from layers.infrastructure.redis_connection import redis_connection
|
||||
from layers.business.sensor_service import SensorService
|
||||
from layers.business.cleanup_service import cleanup_service
|
||||
from layers.presentation.websocket_handler import websocket_manager
|
||||
from layers.presentation.redis_subscriber import redis_subscriber
|
||||
from layers.presentation.api_routes import router as api_router
|
||||
from models import HealthCheck
|
||||
|
||||
# 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 with proper layer initialization"""
|
||||
# Startup
|
||||
logger.info("Application starting up...")
|
||||
|
||||
try:
|
||||
# Initialize infrastructure layer
|
||||
await database_connection.connect()
|
||||
await redis_connection.connect()
|
||||
logger.info("Infrastructure layer initialized")
|
||||
|
||||
# Initialize business layer
|
||||
sensor_service = SensorService() # Services are initialized on-demand
|
||||
logger.info("Business layer initialized")
|
||||
|
||||
# Initialize presentation layer
|
||||
await redis_subscriber.start_subscription("energy_data")
|
||||
await cleanup_service.start_scheduled_cleanup(24) # Daily cleanup
|
||||
logger.info("Presentation layer initialized")
|
||||
|
||||
logger.info("Application startup complete")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
logger.info("Application shutting down...")
|
||||
|
||||
# Stop background tasks
|
||||
await redis_subscriber.stop_subscription()
|
||||
await cleanup_service.stop_scheduled_cleanup()
|
||||
|
||||
# Close connections
|
||||
await database_connection.disconnect()
|
||||
await redis_connection.disconnect()
|
||||
|
||||
logger.info("Application shutdown complete")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during application lifecycle: {e}")
|
||||
raise
|
||||
|
||||
app = FastAPI(
|
||||
title="Energy Monitoring Dashboard API",
|
||||
description="Real-time energy monitoring and IoT sensor data management system (Layered Architecture)",
|
||||
version="2.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 with version prefix
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""
|
||||
WebSocket endpoint for real-time data streaming
|
||||
Presentation Layer - handles WebSocket connections
|
||||
"""
|
||||
await websocket_manager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
# Keep the connection alive by waiting for messages
|
||||
await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
websocket_manager.disconnect(websocket)
|
||||
|
||||
@app.get("/")
|
||||
async def read_root():
|
||||
"""Root endpoint with basic system information"""
|
||||
return {
|
||||
"message": "Energy Monitoring Dashboard Backend (Layered Architecture)",
|
||||
"version": "2.0.0",
|
||||
"status": "running",
|
||||
"uptime_seconds": time.time() - app_start_time,
|
||||
"architecture": "3-layer (Presentation, Business, Infrastructure)"
|
||||
}
|
||||
|
||||
@app.get("/health", response_model=HealthCheck)
|
||||
async def health_check():
|
||||
"""
|
||||
Comprehensive health check endpoint
|
||||
Checks all layers and dependencies
|
||||
"""
|
||||
try:
|
||||
# Check infrastructure layer
|
||||
mongodb_connected = True
|
||||
redis_connected = True
|
||||
|
||||
try:
|
||||
db = await database_connection.get_database()
|
||||
await db.command("ping")
|
||||
except:
|
||||
mongodb_connected = False
|
||||
|
||||
try:
|
||||
redis_client = await redis_connection.get_client()
|
||||
await redis_client.ping()
|
||||
except:
|
||||
redis_connected = False
|
||||
|
||||
# Check business layer through service
|
||||
sensor_service = SensorService()
|
||||
from layers.infrastructure.repositories import SensorReadingRepository
|
||||
stats_repo = SensorReadingRepository()
|
||||
|
||||
# Get basic statistics
|
||||
try:
|
||||
# Simple count queries to test business layer
|
||||
total_readings = await stats_repo.count_by_query({})
|
||||
active_sensors_data = await redis_connection.get_keys_by_pattern("sensor:latest:*")
|
||||
total_sensors = len(active_sensors_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting stats for health check: {e}")
|
||||
total_readings = 0
|
||||
total_sensors = 0
|
||||
|
||||
# Check presentation layer
|
||||
websocket_connections = websocket_manager.get_connection_count()
|
||||
redis_subscription_active = redis_subscriber.is_subscriber_running()
|
||||
|
||||
# Determine overall status
|
||||
status = "healthy"
|
||||
if not mongodb_connected or not redis_connected:
|
||||
status = "degraded"
|
||||
if not mongodb_connected and not redis_connected:
|
||||
status = "unhealthy"
|
||||
|
||||
return HealthCheck(
|
||||
status=status,
|
||||
mongodb_connected=mongodb_connected,
|
||||
redis_connected=redis_connected,
|
||||
total_sensors=total_sensors,
|
||||
active_sensors=total_sensors, # Approximation
|
||||
total_readings=total_readings,
|
||||
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 with layer-specific information
|
||||
"""
|
||||
try:
|
||||
# Infrastructure layer status
|
||||
infrastructure_status = {
|
||||
"database_connected": True,
|
||||
"redis_connected": True
|
||||
}
|
||||
|
||||
try:
|
||||
db = await database_connection.get_database()
|
||||
await db.command("ping")
|
||||
except:
|
||||
infrastructure_status["database_connected"] = False
|
||||
|
||||
try:
|
||||
redis_client = await redis_connection.get_client()
|
||||
await redis_client.ping()
|
||||
except:
|
||||
infrastructure_status["redis_connected"] = False
|
||||
|
||||
# Business layer status
|
||||
business_status = {
|
||||
"cleanup_service_running": cleanup_service.is_cleanup_running()
|
||||
}
|
||||
|
||||
# Presentation layer status
|
||||
presentation_status = {
|
||||
"active_websocket_connections": websocket_manager.get_connection_count(),
|
||||
"redis_subscriber_running": redis_subscriber.is_subscriber_running()
|
||||
}
|
||||
|
||||
# Get subscriber status details
|
||||
subscriber_status = await redis_subscriber.get_subscriber_status()
|
||||
|
||||
return {
|
||||
"timestamp": time.time(),
|
||||
"uptime_seconds": time.time() - app_start_time,
|
||||
"architecture": "layered",
|
||||
"layers": {
|
||||
"infrastructure": infrastructure_status,
|
||||
"business": business_status,
|
||||
"presentation": presentation_status
|
||||
},
|
||||
"redis_subscriber": subscriber_status
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Status check failed: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal Server Error")
|
||||
|
||||
@app.get("/system/cleanup", summary="Get cleanup service status")
|
||||
async def get_cleanup_status():
|
||||
"""Get data cleanup service status and statistics"""
|
||||
try:
|
||||
# Get cleanup service status
|
||||
cleanup_running = cleanup_service.is_cleanup_running()
|
||||
|
||||
# Get storage statistics
|
||||
storage_stats = await cleanup_service.get_storage_statistics()
|
||||
|
||||
# Get retention policy info
|
||||
retention_info = await cleanup_service.get_data_retention_info()
|
||||
|
||||
return {
|
||||
"cleanup_service_running": cleanup_running,
|
||||
"storage_statistics": storage_stats,
|
||||
"retention_policies": retention_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cleanup status: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal Server Error")
|
||||
|
||||
@app.post("/system/cleanup", summary="Run manual cleanup")
|
||||
async def run_manual_cleanup():
|
||||
"""Manually trigger data cleanup process"""
|
||||
try:
|
||||
cleanup_results = await cleanup_service.cleanup_old_data()
|
||||
|
||||
return {
|
||||
"message": "Manual cleanup completed",
|
||||
"results": cleanup_results
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error running manual cleanup: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal Server Error")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
236
models.py
236
models.py
@@ -1,236 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List, Dict, Any, Literal
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
class SensorType(str, Enum):
|
||||
ENERGY = "energy"
|
||||
CO2 = "co2"
|
||||
TEMPERATURE = "temperature"
|
||||
HUMIDITY = "humidity"
|
||||
HVAC = "hvac"
|
||||
LIGHTING = "lighting"
|
||||
SECURITY = "security"
|
||||
|
||||
class SensorStatus(str, Enum):
|
||||
ONLINE = "online"
|
||||
OFFLINE = "offline"
|
||||
ERROR = "error"
|
||||
MAINTENANCE = "maintenance"
|
||||
|
||||
class CO2Status(str, Enum):
|
||||
GOOD = "good"
|
||||
MODERATE = "moderate"
|
||||
POOR = "poor"
|
||||
CRITICAL = "critical"
|
||||
|
||||
class OccupancyLevel(str, Enum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
|
||||
# Base Models
|
||||
class SensorReading(BaseModel):
|
||||
"""Individual sensor reading model"""
|
||||
sensor_id: str = Field(..., description="Unique sensor identifier")
|
||||
room: Optional[str] = Field(None, description="Room where sensor is located")
|
||||
sensor_type: SensorType = Field(..., description="Type of sensor")
|
||||
timestamp: int = Field(..., description="Unix timestamp of reading")
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="Record creation timestamp")
|
||||
|
||||
# Sensor values
|
||||
energy: Optional[Dict[str, Any]] = Field(None, description="Energy reading with value and unit")
|
||||
co2: Optional[Dict[str, Any]] = Field(None, description="CO2 reading with value and unit")
|
||||
temperature: Optional[Dict[str, Any]] = Field(None, description="Temperature reading with value and unit")
|
||||
humidity: Optional[Dict[str, Any]] = Field(None, description="Humidity reading with value and unit")
|
||||
motion: Optional[Dict[str, Any]] = Field(None, description="Motion detection reading")
|
||||
|
||||
# Metadata
|
||||
metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional sensor metadata")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
|
||||
class LegacySensorReading(BaseModel):
|
||||
"""Legacy sensor reading format for backward compatibility"""
|
||||
sensor_id: str = Field(..., alias="sensorId")
|
||||
timestamp: int
|
||||
value: float
|
||||
unit: str
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
class SensorMetadata(BaseModel):
|
||||
"""Sensor configuration and metadata"""
|
||||
sensor_id: str = Field(..., description="Unique sensor identifier")
|
||||
name: str = Field(..., description="Human-readable sensor name")
|
||||
sensor_type: SensorType = Field(..., description="Type of sensor")
|
||||
room: Optional[str] = Field(None, description="Room assignment")
|
||||
status: SensorStatus = Field(default=SensorStatus.OFFLINE, description="Current sensor status")
|
||||
|
||||
# Physical location and installation details
|
||||
location: Optional[str] = Field(None, description="Physical location description")
|
||||
floor: Optional[str] = Field(None, description="Floor level")
|
||||
building: Optional[str] = Field(None, description="Building identifier")
|
||||
|
||||
# Technical specifications
|
||||
model: Optional[str] = Field(None, description="Sensor model")
|
||||
manufacturer: Optional[str] = Field(None, description="Sensor manufacturer")
|
||||
firmware_version: Optional[str] = Field(None, description="Firmware version")
|
||||
hardware_version: Optional[str] = Field(None, description="Hardware version")
|
||||
|
||||
# Network and connectivity
|
||||
ip_address: Optional[str] = Field(None, description="IP address if network connected")
|
||||
mac_address: Optional[str] = Field(None, description="MAC address")
|
||||
connection_type: Optional[str] = Field(None, description="Connection type (wifi, ethernet, zigbee, etc.)")
|
||||
|
||||
# Power and maintenance
|
||||
battery_level: Optional[float] = Field(None, description="Battery level percentage")
|
||||
last_maintenance: Optional[datetime] = Field(None, description="Last maintenance date")
|
||||
next_maintenance: Optional[datetime] = Field(None, description="Next scheduled maintenance")
|
||||
|
||||
# Operational settings
|
||||
sampling_rate: Optional[int] = Field(None, description="Data sampling rate in seconds")
|
||||
calibration_date: Optional[datetime] = Field(None, description="Last calibration date")
|
||||
|
||||
# Capabilities
|
||||
monitoring_capabilities: List[str] = Field(default_factory=list, description="List of monitoring capabilities")
|
||||
control_capabilities: List[str] = Field(default_factory=list, description="List of control capabilities")
|
||||
|
||||
# Timestamps
|
||||
installed_at: Optional[datetime] = Field(None, description="Installation timestamp")
|
||||
last_seen: Optional[datetime] = Field(None, description="Last communication timestamp")
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="Record creation timestamp")
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Record update timestamp")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None
|
||||
}
|
||||
|
||||
class RoomMetrics(BaseModel):
|
||||
"""Aggregated room-level metrics"""
|
||||
room: str = Field(..., description="Room identifier")
|
||||
timestamp: int = Field(..., description="Metrics calculation timestamp")
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="Record creation timestamp")
|
||||
|
||||
# Sensor inventory
|
||||
sensor_count: int = Field(0, description="Total number of sensors in room")
|
||||
active_sensors: List[str] = Field(default_factory=list, description="List of active sensor IDs")
|
||||
sensor_types: List[SensorType] = Field(default_factory=list, description="Types of sensors present")
|
||||
|
||||
# Energy metrics
|
||||
energy: Optional[Dict[str, Any]] = Field(None, description="Energy consumption metrics")
|
||||
# Format: {"current": float, "total": float, "average": float, "peak": float, "unit": str}
|
||||
|
||||
# Environmental metrics
|
||||
co2: Optional[Dict[str, Any]] = Field(None, description="CO2 level metrics")
|
||||
# Format: {"current": float, "average": float, "max": float, "min": float, "status": CO2Status, "unit": str}
|
||||
|
||||
temperature: Optional[Dict[str, Any]] = Field(None, description="Temperature metrics")
|
||||
# Format: {"current": float, "average": float, "max": float, "min": float, "unit": str}
|
||||
|
||||
humidity: Optional[Dict[str, Any]] = Field(None, description="Humidity metrics")
|
||||
# Format: {"current": float, "average": float, "max": float, "min": float, "unit": str}
|
||||
|
||||
# Occupancy and usage
|
||||
occupancy_estimate: OccupancyLevel = Field(default=OccupancyLevel.LOW, description="Estimated occupancy level")
|
||||
motion_detected: bool = Field(default=False, description="Recent motion detection status")
|
||||
|
||||
# Time-based metrics
|
||||
last_activity: Optional[datetime] = Field(None, description="Last detected activity timestamp")
|
||||
daily_usage_hours: Optional[float] = Field(None, description="Estimated daily usage in hours")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None
|
||||
}
|
||||
|
||||
class SystemEvent(BaseModel):
|
||||
"""System events and alerts"""
|
||||
event_id: str = Field(..., description="Unique event identifier")
|
||||
event_type: str = Field(..., description="Type of event")
|
||||
severity: Literal["info", "warning", "error", "critical"] = Field(..., description="Event severity")
|
||||
timestamp: int = Field(..., description="Event timestamp")
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow, description="Record creation timestamp")
|
||||
|
||||
# Event details
|
||||
title: str = Field(..., description="Event title")
|
||||
description: str = Field(..., description="Event description")
|
||||
source: Optional[str] = Field(None, description="Event source (sensor_id, system component, etc.)")
|
||||
|
||||
# Context
|
||||
sensor_id: Optional[str] = Field(None, description="Related sensor ID")
|
||||
room: Optional[str] = Field(None, description="Related room")
|
||||
|
||||
# Event data
|
||||
data: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional event data")
|
||||
|
||||
# Status tracking
|
||||
acknowledged: bool = Field(default=False, description="Whether event has been acknowledged")
|
||||
resolved: bool = Field(default=False, description="Whether event has been resolved")
|
||||
acknowledged_by: Optional[str] = Field(None, description="Who acknowledged the event")
|
||||
resolved_by: Optional[str] = Field(None, description="Who resolved the event")
|
||||
acknowledged_at: Optional[datetime] = Field(None, description="Acknowledgment timestamp")
|
||||
resolved_at: Optional[datetime] = Field(None, description="Resolution timestamp")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None
|
||||
}
|
||||
|
||||
class DataQuery(BaseModel):
|
||||
"""Data query parameters for historical data retrieval"""
|
||||
sensor_ids: Optional[List[str]] = Field(None, description="Filter by sensor IDs")
|
||||
rooms: Optional[List[str]] = Field(None, description="Filter by rooms")
|
||||
sensor_types: Optional[List[SensorType]] = Field(None, description="Filter by sensor types")
|
||||
|
||||
# Time range
|
||||
start_time: Optional[int] = Field(None, description="Start timestamp (Unix)")
|
||||
end_time: Optional[int] = Field(None, description="End timestamp (Unix)")
|
||||
|
||||
# Aggregation
|
||||
aggregate: Optional[str] = Field(None, description="Aggregation method (avg, sum, min, max)")
|
||||
interval: Optional[str] = Field(None, description="Aggregation interval (1m, 5m, 1h, 1d)")
|
||||
|
||||
# Pagination
|
||||
limit: int = Field(default=100, description="Maximum number of records to return")
|
||||
offset: int = Field(default=0, description="Number of records to skip")
|
||||
|
||||
# Sorting
|
||||
sort_by: str = Field(default="timestamp", description="Field to sort by")
|
||||
sort_order: Literal["asc", "desc"] = Field(default="desc", description="Sort order")
|
||||
|
||||
class DataResponse(BaseModel):
|
||||
"""Response model for data queries"""
|
||||
data: List[Dict[str, Any]] = Field(default_factory=list, description="Query results")
|
||||
total_count: int = Field(0, description="Total number of matching records")
|
||||
query: DataQuery = Field(..., description="Original query parameters")
|
||||
execution_time_ms: float = Field(..., description="Query execution time in milliseconds")
|
||||
|
||||
class HealthCheck(BaseModel):
|
||||
"""Health check response model"""
|
||||
status: str = Field(..., description="Overall system status")
|
||||
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Database status
|
||||
mongodb_connected: bool = Field(..., description="MongoDB connection status")
|
||||
redis_connected: bool = Field(..., description="Redis connection status")
|
||||
|
||||
# Data statistics
|
||||
total_sensors: int = Field(0, description="Total number of registered sensors")
|
||||
active_sensors: int = Field(0, description="Number of active sensors")
|
||||
total_readings: int = Field(0, description="Total sensor readings in database")
|
||||
|
||||
# System metrics
|
||||
uptime_seconds: float = Field(..., description="System uptime in seconds")
|
||||
memory_usage_mb: Optional[float] = Field(None, description="Memory usage in MB")
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat()
|
||||
}
|
||||
448
persistence.py
448
persistence.py
@@ -1,448 +0,0 @@
|
||||
import json
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, List, Optional
|
||||
import logging
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
import uuid
|
||||
|
||||
from database import get_database, redis_manager
|
||||
from models import (
|
||||
SensorReading, LegacySensorReading, SensorMetadata, RoomMetrics,
|
||||
SystemEvent, SensorType, SensorStatus, CO2Status, OccupancyLevel
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DataPersistenceService:
|
||||
"""Service for persisting sensor data to MongoDB and managing Redis cache"""
|
||||
|
||||
def __init__(self):
|
||||
self.db = None
|
||||
self.redis = redis_manager
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the persistence service"""
|
||||
self.db = await get_database()
|
||||
await self.redis.connect()
|
||||
logger.info("Data persistence service initialized")
|
||||
|
||||
async def process_sensor_message(self, message_data: str) -> bool:
|
||||
"""Process incoming sensor message and persist data"""
|
||||
try:
|
||||
# Parse the message
|
||||
data = json.loads(message_data)
|
||||
logger.debug(f"Processing sensor message: {data}")
|
||||
|
||||
# Determine message format and convert to standard format
|
||||
if self._is_legacy_format(data):
|
||||
sensor_reading = await self._convert_legacy_data(data)
|
||||
else:
|
||||
sensor_reading = SensorReading(**data)
|
||||
|
||||
# Store in MongoDB
|
||||
await self._store_sensor_reading(sensor_reading)
|
||||
|
||||
# Update Redis cache for real-time access
|
||||
await self._update_redis_cache(sensor_reading)
|
||||
|
||||
# Update sensor metadata
|
||||
await self._update_sensor_metadata(sensor_reading)
|
||||
|
||||
# Calculate and store room metrics
|
||||
await self._update_room_metrics(sensor_reading)
|
||||
|
||||
# Check for alerts and anomalies
|
||||
await self._check_alerts(sensor_reading)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing sensor message: {e}")
|
||||
# Log the error event
|
||||
await self._log_system_event(
|
||||
event_type="data_processing_error",
|
||||
severity="error",
|
||||
title="Sensor Data Processing Failed",
|
||||
description=f"Failed to process sensor message: {str(e)}",
|
||||
data={"raw_message": message_data}
|
||||
)
|
||||
return False
|
||||
|
||||
def _is_legacy_format(self, data: dict) -> bool:
|
||||
"""Check if data is in legacy format"""
|
||||
legacy_keys = {"sensorId", "timestamp", "value", "unit"}
|
||||
return legacy_keys.issubset(data.keys()) and "energy" not in data
|
||||
|
||||
async def _convert_legacy_data(self, data: dict) -> SensorReading:
|
||||
"""Convert legacy format to new sensor reading format"""
|
||||
legacy_reading = LegacySensorReading(**data)
|
||||
|
||||
return SensorReading(
|
||||
sensor_id=legacy_reading.sensor_id,
|
||||
sensor_type=SensorType.ENERGY, # Assume legacy data is energy
|
||||
timestamp=legacy_reading.timestamp,
|
||||
created_at=legacy_reading.created_at,
|
||||
energy={
|
||||
"value": legacy_reading.value,
|
||||
"unit": legacy_reading.unit
|
||||
}
|
||||
)
|
||||
|
||||
async def _store_sensor_reading(self, reading: SensorReading):
|
||||
"""Store sensor reading in MongoDB"""
|
||||
try:
|
||||
reading_dict = reading.dict()
|
||||
|
||||
# Add document ID for deduplication
|
||||
reading_dict["_id"] = f"{reading.sensor_id}_{reading.timestamp}"
|
||||
|
||||
await self.db.sensor_readings.insert_one(reading_dict)
|
||||
logger.debug(f"Stored sensor reading for {reading.sensor_id}")
|
||||
|
||||
except DuplicateKeyError:
|
||||
logger.debug(f"Duplicate reading ignored for {reading.sensor_id} at {reading.timestamp}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error storing sensor reading: {e}")
|
||||
raise
|
||||
|
||||
async def _update_redis_cache(self, reading: SensorReading):
|
||||
"""Update Redis cache with latest sensor data"""
|
||||
try:
|
||||
# Store latest reading for real-time access
|
||||
await self.redis.set_sensor_data(
|
||||
reading.sensor_id,
|
||||
reading.dict(),
|
||||
expire_time=3600 # 1 hour expiration
|
||||
)
|
||||
|
||||
# Store sensor status
|
||||
status_key = f"sensor:status:{reading.sensor_id}"
|
||||
await self.redis.redis_client.setex(
|
||||
status_key,
|
||||
1800, # 30 minutes
|
||||
json.dumps({
|
||||
"status": "online",
|
||||
"last_seen": reading.timestamp,
|
||||
"room": reading.room
|
||||
})
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating Redis cache: {e}")
|
||||
|
||||
async def _update_sensor_metadata(self, reading: SensorReading):
|
||||
"""Update or create sensor metadata"""
|
||||
try:
|
||||
# Check if sensor metadata exists
|
||||
existing = await self.db.sensor_metadata.find_one({"sensor_id": reading.sensor_id})
|
||||
|
||||
if existing:
|
||||
# Update existing metadata
|
||||
await self.db.sensor_metadata.update_one(
|
||||
{"sensor_id": reading.sensor_id},
|
||||
{
|
||||
"$set": {
|
||||
"last_seen": datetime.utcnow(),
|
||||
"status": SensorStatus.ONLINE.value,
|
||||
"updated_at": datetime.utcnow()
|
||||
},
|
||||
"$addToSet": {
|
||||
"monitoring_capabilities": reading.sensor_type.value
|
||||
}
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Create new sensor metadata
|
||||
metadata = SensorMetadata(
|
||||
sensor_id=reading.sensor_id,
|
||||
name=f"Sensor {reading.sensor_id}",
|
||||
sensor_type=reading.sensor_type,
|
||||
room=reading.room,
|
||||
status=SensorStatus.ONLINE,
|
||||
last_seen=datetime.utcnow(),
|
||||
monitoring_capabilities=[reading.sensor_type.value]
|
||||
)
|
||||
|
||||
await self.db.sensor_metadata.insert_one(metadata.dict())
|
||||
logger.info(f"Created metadata for new sensor: {reading.sensor_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating sensor metadata: {e}")
|
||||
|
||||
async def _update_room_metrics(self, reading: SensorReading):
|
||||
"""Calculate and store room-level metrics"""
|
||||
if not reading.room:
|
||||
return
|
||||
|
||||
try:
|
||||
# Get recent readings for this room (last 5 minutes)
|
||||
recent_time = datetime.utcnow() - timedelta(minutes=5)
|
||||
|
||||
# Query recent readings for the room
|
||||
cursor = self.db.sensor_readings.find({
|
||||
"room": reading.room,
|
||||
"created_at": {"$gte": recent_time}
|
||||
})
|
||||
|
||||
recent_readings = await cursor.to_list(length=None)
|
||||
|
||||
if not recent_readings:
|
||||
return
|
||||
|
||||
# Calculate aggregated metrics
|
||||
metrics = await self._calculate_room_metrics(reading.room, recent_readings)
|
||||
|
||||
# Store in MongoDB
|
||||
await self.db.room_metrics.insert_one(metrics.dict())
|
||||
|
||||
# Cache in Redis
|
||||
await self.redis.set_room_metrics(reading.room, metrics.dict())
|
||||
|
||||
logger.debug(f"Updated room metrics for {reading.room}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating room metrics: {e}")
|
||||
|
||||
async def _calculate_room_metrics(self, room: str, readings: List[Dict]) -> RoomMetrics:
|
||||
"""Calculate aggregated metrics for a room"""
|
||||
|
||||
# Group readings by sensor
|
||||
sensors_data = {}
|
||||
for reading in readings:
|
||||
sensor_id = reading["sensor_id"]
|
||||
if sensor_id not in sensors_data:
|
||||
sensors_data[sensor_id] = []
|
||||
sensors_data[sensor_id].append(reading)
|
||||
|
||||
# Initialize metrics
|
||||
energy_values = []
|
||||
co2_values = []
|
||||
temperature_values = []
|
||||
humidity_values = []
|
||||
motion_detected = False
|
||||
|
||||
# Extract values from readings
|
||||
for sensor_readings in sensors_data.values():
|
||||
for reading in sensor_readings:
|
||||
if reading.get("energy"):
|
||||
energy_values.append(reading["energy"]["value"])
|
||||
if reading.get("co2"):
|
||||
co2_values.append(reading["co2"]["value"])
|
||||
if reading.get("temperature"):
|
||||
temperature_values.append(reading["temperature"]["value"])
|
||||
if reading.get("humidity"):
|
||||
humidity_values.append(reading["humidity"]["value"])
|
||||
if reading.get("motion") and reading["motion"].get("value") == "Detected":
|
||||
motion_detected = True
|
||||
|
||||
# Calculate aggregated metrics
|
||||
metrics = RoomMetrics(
|
||||
room=room,
|
||||
timestamp=int(datetime.utcnow().timestamp()),
|
||||
sensor_count=len(sensors_data),
|
||||
active_sensors=list(sensors_data.keys()),
|
||||
sensor_types=list(set(reading.get("sensor_type") for reading in readings if reading.get("sensor_type"))),
|
||||
motion_detected=motion_detected
|
||||
)
|
||||
|
||||
# Energy metrics
|
||||
if energy_values:
|
||||
metrics.energy = {
|
||||
"current": sum(energy_values),
|
||||
"average": sum(energy_values) / len(energy_values),
|
||||
"total": sum(energy_values),
|
||||
"peak": max(energy_values),
|
||||
"unit": "kWh"
|
||||
}
|
||||
|
||||
# CO2 metrics
|
||||
if co2_values:
|
||||
avg_co2 = sum(co2_values) / len(co2_values)
|
||||
metrics.co2 = {
|
||||
"current": avg_co2,
|
||||
"average": avg_co2,
|
||||
"max": max(co2_values),
|
||||
"min": min(co2_values),
|
||||
"status": self._get_co2_status(avg_co2).value,
|
||||
"unit": "ppm"
|
||||
}
|
||||
|
||||
# Set occupancy estimate based on CO2
|
||||
metrics.occupancy_estimate = self._estimate_occupancy(avg_co2)
|
||||
|
||||
# Temperature metrics
|
||||
if temperature_values:
|
||||
metrics.temperature = {
|
||||
"current": sum(temperature_values) / len(temperature_values),
|
||||
"average": sum(temperature_values) / len(temperature_values),
|
||||
"max": max(temperature_values),
|
||||
"min": min(temperature_values),
|
||||
"unit": "°C"
|
||||
}
|
||||
|
||||
# Humidity metrics
|
||||
if humidity_values:
|
||||
metrics.humidity = {
|
||||
"current": sum(humidity_values) / len(humidity_values),
|
||||
"average": sum(humidity_values) / len(humidity_values),
|
||||
"max": max(humidity_values),
|
||||
"min": min(humidity_values),
|
||||
"unit": "%"
|
||||
}
|
||||
|
||||
return metrics
|
||||
|
||||
def _get_co2_status(self, co2_level: float) -> CO2Status:
|
||||
"""Determine CO2 status based on level"""
|
||||
if co2_level < 400:
|
||||
return CO2Status.GOOD
|
||||
elif co2_level < 1000:
|
||||
return CO2Status.MODERATE
|
||||
elif co2_level < 5000:
|
||||
return CO2Status.POOR
|
||||
else:
|
||||
return CO2Status.CRITICAL
|
||||
|
||||
def _estimate_occupancy(self, co2_level: float) -> OccupancyLevel:
|
||||
"""Estimate occupancy level based on CO2"""
|
||||
if co2_level < 600:
|
||||
return OccupancyLevel.LOW
|
||||
elif co2_level < 1200:
|
||||
return OccupancyLevel.MEDIUM
|
||||
else:
|
||||
return OccupancyLevel.HIGH
|
||||
|
||||
async def _check_alerts(self, reading: SensorReading):
|
||||
"""Check for alert conditions and create system events"""
|
||||
alerts = []
|
||||
|
||||
# CO2 level alerts
|
||||
if reading.co2:
|
||||
co2_level = reading.co2.get("value", 0)
|
||||
if co2_level > 5000:
|
||||
alerts.append({
|
||||
"event_type": "co2_critical",
|
||||
"severity": "critical",
|
||||
"title": "Critical CO2 Level",
|
||||
"description": f"CO2 level ({co2_level} ppm) exceeds critical threshold in {reading.room or 'unknown room'}"
|
||||
})
|
||||
elif co2_level > 1000:
|
||||
alerts.append({
|
||||
"event_type": "co2_high",
|
||||
"severity": "warning",
|
||||
"title": "High CO2 Level",
|
||||
"description": f"CO2 level ({co2_level} ppm) is above recommended levels in {reading.room or 'unknown room'}"
|
||||
})
|
||||
|
||||
# Energy consumption alerts
|
||||
if reading.energy:
|
||||
energy_value = reading.energy.get("value", 0)
|
||||
if energy_value > 10: # Threshold for high energy consumption
|
||||
alerts.append({
|
||||
"event_type": "energy_high",
|
||||
"severity": "warning",
|
||||
"title": "High Energy Consumption",
|
||||
"description": f"Energy consumption ({energy_value} kWh) is unusually high for sensor {reading.sensor_id}"
|
||||
})
|
||||
|
||||
# Temperature alerts
|
||||
if reading.temperature:
|
||||
temp_value = reading.temperature.get("value", 0)
|
||||
if temp_value > 30 or temp_value < 15:
|
||||
alerts.append({
|
||||
"event_type": "temperature_extreme",
|
||||
"severity": "warning",
|
||||
"title": "Extreme Temperature",
|
||||
"description": f"Temperature ({temp_value}°C) is outside normal range in {reading.room or 'unknown room'}"
|
||||
})
|
||||
|
||||
# Create system events for alerts
|
||||
for alert in alerts:
|
||||
await self._log_system_event(
|
||||
sensor_id=reading.sensor_id,
|
||||
room=reading.room,
|
||||
**alert,
|
||||
data=reading.dict()
|
||||
)
|
||||
|
||||
async def _log_system_event(self, event_type: str, severity: str, title: str, description: str,
|
||||
sensor_id: str = None, room: str = None, source: str = None, data: Dict = None):
|
||||
"""Log a system event"""
|
||||
try:
|
||||
event = SystemEvent(
|
||||
event_id=str(uuid.uuid4()),
|
||||
event_type=event_type,
|
||||
severity=severity,
|
||||
timestamp=int(datetime.utcnow().timestamp()),
|
||||
title=title,
|
||||
description=description,
|
||||
sensor_id=sensor_id,
|
||||
room=room,
|
||||
source=source or "data_persistence_service",
|
||||
data=data or {}
|
||||
)
|
||||
|
||||
await self.db.system_events.insert_one(event.dict())
|
||||
logger.info(f"System event logged: {event_type} - {title}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error logging system event: {e}")
|
||||
|
||||
async def get_recent_readings(self, sensor_id: str = None, room: str = None,
|
||||
limit: int = 100, minutes: int = 60) -> List[Dict]:
|
||||
"""Get recent sensor readings"""
|
||||
try:
|
||||
# Build query
|
||||
query = {
|
||||
"created_at": {"$gte": datetime.utcnow() - timedelta(minutes=minutes)}
|
||||
}
|
||||
|
||||
if sensor_id:
|
||||
query["sensor_id"] = sensor_id
|
||||
if room:
|
||||
query["room"] = room
|
||||
|
||||
cursor = self.db.sensor_readings.find(query).sort("created_at", -1).limit(limit)
|
||||
readings = await cursor.to_list(length=limit)
|
||||
|
||||
return readings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting recent readings: {e}")
|
||||
return []
|
||||
|
||||
async def get_sensor_statistics(self) -> Dict[str, Any]:
|
||||
"""Get overall sensor statistics"""
|
||||
try:
|
||||
stats = {}
|
||||
|
||||
# Total readings count
|
||||
stats["total_readings"] = await self.db.sensor_readings.count_documents({})
|
||||
|
||||
# Active sensors (sensors that sent data in last 24 hours)
|
||||
recent_time = datetime.utcnow() - timedelta(hours=24)
|
||||
active_sensors = await self.db.sensor_readings.distinct("sensor_id", {
|
||||
"created_at": {"$gte": recent_time}
|
||||
})
|
||||
stats["active_sensors"] = len(active_sensors)
|
||||
|
||||
# Total registered sensors
|
||||
stats["total_sensors"] = await self.db.sensor_metadata.count_documents({})
|
||||
|
||||
# Readings in last 24 hours
|
||||
stats["recent_readings"] = await self.db.sensor_readings.count_documents({
|
||||
"created_at": {"$gte": recent_time}
|
||||
})
|
||||
|
||||
# Room count
|
||||
stats["total_rooms"] = len(await self.db.sensor_readings.distinct("room", {"room": {"$ne": None}}))
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting sensor statistics: {e}")
|
||||
return {}
|
||||
|
||||
# Global persistence service instance
|
||||
persistence_service = DataPersistenceService()
|
||||
@@ -1,10 +0,0 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
redis
|
||||
websockets
|
||||
pymongo
|
||||
motor
|
||||
python-dotenv
|
||||
pandas
|
||||
numpy
|
||||
pydantic
|
||||
Reference in New Issue
Block a user