262 lines
9.3 KiB
Python
262 lines
9.3 KiB
Python
"""
|
|
Room metrics business logic service
|
|
Business Layer - handles room-related aggregations and business operations
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, Any, List, Optional
|
|
import logging
|
|
|
|
from models import RoomMetrics, CO2Status, OccupancyLevel
|
|
from ..infrastructure.repositories import (
|
|
SensorReadingRepository, RoomMetricsRepository, RedisRepository
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class RoomService:
|
|
"""Service for room-related business operations"""
|
|
|
|
def __init__(self):
|
|
self.sensor_reading_repo = SensorReadingRepository()
|
|
self.room_metrics_repo = RoomMetricsRepository()
|
|
self.redis_repo = RedisRepository()
|
|
|
|
async def update_room_metrics(self, room: str) -> bool:
|
|
"""Calculate and store room-level metrics"""
|
|
if not room:
|
|
return False
|
|
|
|
try:
|
|
# Get recent readings for this room (last 5 minutes)
|
|
recent_readings = await self.sensor_reading_repo.get_recent_by_room(
|
|
room=room,
|
|
minutes=5
|
|
)
|
|
|
|
if not recent_readings:
|
|
return False
|
|
|
|
# Calculate aggregated metrics
|
|
metrics = await self._calculate_room_metrics(room, recent_readings)
|
|
|
|
# Store in MongoDB
|
|
stored = await self.room_metrics_repo.create(metrics)
|
|
|
|
# Cache in Redis
|
|
if stored:
|
|
await self.redis_repo.set_room_metrics(room, metrics.dict())
|
|
logger.debug(f"Updated room metrics for {room}")
|
|
|
|
return stored
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating room metrics for {room}: {e}")
|
|
return False
|
|
|
|
async def _calculate_room_metrics(self, room: str, readings: List[Dict]) -> RoomMetrics:
|
|
"""Calculate aggregated metrics for a room based on recent readings"""
|
|
|
|
# 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 value arrays
|
|
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
|
|
|
|
# Get sensor types present
|
|
sensor_types = list(set(
|
|
reading.get("sensor_type")
|
|
for reading in readings
|
|
if reading.get("sensor_type")
|
|
))
|
|
|
|
# Initialize metrics object
|
|
metrics = RoomMetrics(
|
|
room=room,
|
|
timestamp=int(datetime.utcnow().timestamp()),
|
|
sensor_count=len(sensors_data),
|
|
active_sensors=list(sensors_data.keys()),
|
|
sensor_types=sensor_types,
|
|
motion_detected=motion_detected
|
|
)
|
|
|
|
# Calculate energy metrics
|
|
if energy_values:
|
|
metrics.energy = self._calculate_energy_metrics(energy_values)
|
|
|
|
# Calculate CO2 metrics and occupancy
|
|
if co2_values:
|
|
metrics.co2 = self._calculate_co2_metrics(co2_values)
|
|
metrics.occupancy_estimate = self._estimate_occupancy_from_co2(
|
|
metrics.co2["average"]
|
|
)
|
|
|
|
# Calculate temperature metrics
|
|
if temperature_values:
|
|
metrics.temperature = self._calculate_temperature_metrics(temperature_values)
|
|
|
|
# Calculate humidity metrics
|
|
if humidity_values:
|
|
metrics.humidity = self._calculate_humidity_metrics(humidity_values)
|
|
|
|
# Set last activity time if motion detected
|
|
if motion_detected:
|
|
metrics.last_activity = datetime.utcnow()
|
|
|
|
return metrics
|
|
|
|
def _calculate_energy_metrics(self, values: List[float]) -> Dict[str, Any]:
|
|
"""Calculate energy consumption metrics"""
|
|
return {
|
|
"current": sum(values),
|
|
"average": sum(values) / len(values),
|
|
"total": sum(values),
|
|
"peak": max(values),
|
|
"unit": "kWh"
|
|
}
|
|
|
|
def _calculate_co2_metrics(self, values: List[float]) -> Dict[str, Any]:
|
|
"""Calculate CO2 level metrics"""
|
|
avg_co2 = sum(values) / len(values)
|
|
return {
|
|
"current": avg_co2,
|
|
"average": avg_co2,
|
|
"max": max(values),
|
|
"min": min(values),
|
|
"status": self._get_co2_status(avg_co2).value,
|
|
"unit": "ppm"
|
|
}
|
|
|
|
def _calculate_temperature_metrics(self, values: List[float]) -> Dict[str, Any]:
|
|
"""Calculate temperature metrics"""
|
|
avg_temp = sum(values) / len(values)
|
|
return {
|
|
"current": avg_temp,
|
|
"average": avg_temp,
|
|
"max": max(values),
|
|
"min": min(values),
|
|
"unit": "°C"
|
|
}
|
|
|
|
def _calculate_humidity_metrics(self, values: List[float]) -> Dict[str, Any]:
|
|
"""Calculate humidity metrics"""
|
|
avg_humidity = sum(values) / len(values)
|
|
return {
|
|
"current": avg_humidity,
|
|
"average": avg_humidity,
|
|
"max": max(values),
|
|
"min": min(values),
|
|
"unit": "%"
|
|
}
|
|
|
|
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_from_co2(self, co2_level: float) -> OccupancyLevel:
|
|
"""Estimate occupancy level based on CO2 levels"""
|
|
if co2_level < 600:
|
|
return OccupancyLevel.LOW
|
|
elif co2_level < 1200:
|
|
return OccupancyLevel.MEDIUM
|
|
else:
|
|
return OccupancyLevel.HIGH
|
|
|
|
async def get_all_rooms(self) -> Dict[str, Any]:
|
|
"""Get list of all rooms with sensor counts and latest metrics"""
|
|
try:
|
|
rooms = await self.sensor_reading_repo.get_distinct_rooms()
|
|
|
|
room_data = []
|
|
for room in rooms:
|
|
# Get sensor count for each room
|
|
sensor_ids = await self.sensor_reading_repo.get_distinct_sensor_ids_by_room(room)
|
|
sensor_count = len(sensor_ids)
|
|
|
|
# Get latest room metrics from cache
|
|
room_metrics = await self.redis_repo.get_room_metrics(room)
|
|
|
|
room_data.append({
|
|
"room": room,
|
|
"sensor_count": sensor_count,
|
|
"sensor_ids": sensor_ids,
|
|
"latest_metrics": room_metrics
|
|
})
|
|
|
|
return {
|
|
"rooms": room_data,
|
|
"count": len(room_data)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting rooms: {e}")
|
|
return {"rooms": [], "count": 0}
|
|
|
|
async def get_room_data(self, room_name: str, start_time: Optional[int] = None,
|
|
end_time: Optional[int] = None, limit: int = 100) -> Dict[str, Any]:
|
|
"""Get historical data for a specific room"""
|
|
try:
|
|
# Build query for time range
|
|
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
|
|
room_metrics = await self.room_metrics_repo.get_by_room(room_name, limit)
|
|
|
|
# Get sensor readings for the room
|
|
sensor_readings = await self.sensor_reading_repo.get_by_query(
|
|
query=query,
|
|
sort_by="timestamp",
|
|
sort_order="desc",
|
|
limit=limit
|
|
)
|
|
|
|
return {
|
|
"room": room_name,
|
|
"room_metrics": room_metrics,
|
|
"sensor_readings": sensor_readings
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting room data for {room_name}: {e}")
|
|
return {
|
|
"room": room_name,
|
|
"room_metrics": [],
|
|
"sensor_readings": []
|
|
} |