""" Analytics business logic service Business Layer - handles analytics calculations and data aggregations """ from datetime import datetime, timedelta from typing import Dict, Any, List, Optional import logging from ..infrastructure.repositories import SensorReadingRepository logger = logging.getLogger(__name__) class AnalyticsService: """Service for analytics and reporting operations""" def __init__(self): self.sensor_reading_repo = SensorReadingRepository() async def get_analytics_summary(self, hours: int = 24) -> Dict[str, Any]: """Get comprehensive analytics summary for the specified time period""" try: start_time = datetime.utcnow() - timedelta(hours=hours) # Sensor-level analytics pipeline sensor_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}} ] sensor_analytics = await self.sensor_reading_repo.aggregate(sensor_pipeline) # Room-level analytics pipeline 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_analytics = await self.sensor_reading_repo.aggregate(room_pipeline) # Calculate summary statistics summary_stats = self._calculate_summary_stats(sensor_analytics, room_analytics) return { "period_hours": hours, "start_time": start_time.isoformat(), "sensor_analytics": sensor_analytics, "room_analytics": room_analytics, "summary": summary_stats } except Exception as e: logger.error(f"Error getting analytics summary: {e}") return { "period_hours": hours, "start_time": None, "sensor_analytics": [], "room_analytics": [], "summary": {} } def _calculate_summary_stats(self, sensor_analytics: List[Dict], room_analytics: List[Dict]) -> Dict[str, Any]: """Calculate summary statistics from analytics data""" total_readings = sum(item["reading_count"] for item in sensor_analytics) total_energy = sum(item.get("total_energy", 0) or 0 for item in sensor_analytics) # Energy consumption insights energy_insights = { "total_consumption_kwh": round(total_energy, 2), "average_consumption_per_sensor": ( round(total_energy / len(sensor_analytics), 2) if sensor_analytics else 0 ), "top_energy_consumer": ( sensor_analytics[0]["_id"]["sensor_id"] if sensor_analytics else None ) } # CO2 insights co2_values = [item.get("avg_co2") for item in sensor_analytics if item.get("avg_co2")] co2_insights = { "average_co2_level": ( round(sum(co2_values) / len(co2_values), 1) if co2_values else 0 ), "sensors_with_high_co2": len([ co2 for co2 in co2_values if co2 and co2 > 1000 ]), "sensors_with_critical_co2": len([ co2 for co2 in co2_values if co2 and co2 > 5000 ]) } return { "total_sensors_analyzed": len(sensor_analytics), "total_rooms_analyzed": len(room_analytics), "total_readings": total_readings, "energy_insights": energy_insights, "co2_insights": co2_insights } async def get_energy_trends(self, hours: int = 168) -> Dict[str, Any]: """Get energy consumption trends (default: last week)""" try: start_time = datetime.utcnow() - timedelta(hours=hours) # Hourly energy consumption pipeline pipeline = [ {"$match": { "created_at": {"$gte": start_time}, "energy.value": {"$exists": True} }}, {"$group": { "_id": { "year": {"$year": "$created_at"}, "month": {"$month": "$created_at"}, "day": {"$dayOfMonth": "$created_at"}, "hour": {"$hour": "$created_at"} }, "total_energy": {"$sum": "$energy.value"}, "sensor_count": {"$addToSet": "$sensor_id"}, "reading_count": {"$sum": 1} }}, {"$project": { "_id": 0, "timestamp": { "$dateFromParts": { "year": "$_id.year", "month": "$_id.month", "day": "$_id.day", "hour": "$_id.hour" } }, "total_energy": {"$round": ["$total_energy", 2]}, "sensor_count": {"$size": "$sensor_count"}, "reading_count": 1 }}, {"$sort": {"timestamp": 1}} ] trends = await self.sensor_reading_repo.aggregate(pipeline) # Calculate trend insights insights = self._calculate_trend_insights(trends) return { "period_hours": hours, "data_points": len(trends), "trends": trends, "insights": insights } except Exception as e: logger.error(f"Error getting energy trends: {e}") return { "period_hours": hours, "data_points": 0, "trends": [], "insights": {} } def _calculate_trend_insights(self, trends: List[Dict]) -> Dict[str, Any]: """Calculate insights from trend data""" if not trends: return {} energy_values = [item["total_energy"] for item in trends] # Peak and low consumption max_consumption = max(energy_values) min_consumption = min(energy_values) avg_consumption = sum(energy_values) / len(energy_values) # Find peak time peak_item = max(trends, key=lambda x: x["total_energy"]) peak_time = peak_item["timestamp"] return { "peak_consumption_kwh": max_consumption, "lowest_consumption_kwh": min_consumption, "average_consumption_kwh": round(avg_consumption, 2), "peak_time": peak_time.isoformat() if hasattr(peak_time, 'isoformat') else str(peak_time), "consumption_variance": round(max_consumption - min_consumption, 2) } async def get_room_comparison(self, hours: int = 24) -> Dict[str, Any]: """Get room-by-room comparison analytics""" try: start_time = datetime.utcnow() - timedelta(hours=hours) pipeline = [ {"$match": { "created_at": {"$gte": start_time}, "room": {"$ne": None} }}, {"$group": { "_id": "$room", "total_energy": {"$sum": "$energy.value"}, "avg_energy": {"$avg": "$energy.value"}, "avg_co2": {"$avg": "$co2.value"}, "max_co2": {"$max": "$co2.value"}, "avg_temperature": {"$avg": "$temperature.value"}, "sensor_count": {"$addToSet": "$sensor_id"}, "reading_count": {"$sum": 1} }}, {"$project": { "room": "$_id", "_id": 0, "total_energy": {"$round": [{"$ifNull": ["$total_energy", 0]}, 2]}, "avg_energy": {"$round": [{"$ifNull": ["$avg_energy", 0]}, 2]}, "avg_co2": {"$round": [{"$ifNull": ["$avg_co2", 0]}, 1]}, "max_co2": {"$round": [{"$ifNull": ["$max_co2", 0]}, 1]}, "avg_temperature": {"$round": [{"$ifNull": ["$avg_temperature", 0]}, 1]}, "sensor_count": {"$size": "$sensor_count"}, "reading_count": 1 }}, {"$sort": {"total_energy": -1}} ] room_comparison = await self.sensor_reading_repo.aggregate(pipeline) # Calculate comparison insights insights = self._calculate_room_insights(room_comparison) return { "period_hours": hours, "rooms_analyzed": len(room_comparison), "comparison": room_comparison, "insights": insights } except Exception as e: logger.error(f"Error getting room comparison: {e}") return { "period_hours": hours, "rooms_analyzed": 0, "comparison": [], "insights": {} } def _calculate_room_insights(self, room_data: List[Dict]) -> Dict[str, Any]: """Calculate insights from room comparison data""" if not room_data: return {} # Energy insights total_energy = sum(room["total_energy"] for room in room_data) highest_consumer = room_data[0] if room_data else None lowest_consumer = min(room_data, key=lambda x: x["total_energy"]) if room_data else None # CO2 insights rooms_with_high_co2 = [ room for room in room_data if room.get("avg_co2", 0) > 1000 ] # Temperature insights temp_values = [room.get("avg_temperature", 0) for room in room_data if room.get("avg_temperature")] avg_building_temp = sum(temp_values) / len(temp_values) if temp_values else 0 return { "total_building_energy_kwh": round(total_energy, 2), "highest_energy_consumer": highest_consumer["room"] if highest_consumer else None, "lowest_energy_consumer": lowest_consumer["room"] if lowest_consumer else None, "rooms_with_high_co2": len(rooms_with_high_co2), "high_co2_rooms": [room["room"] for room in rooms_with_high_co2], "average_building_temperature": round(avg_building_temp, 1), "total_active_sensors": sum(room["sensor_count"] for room in room_data) }