300 lines
12 KiB
Python
300 lines
12 KiB
Python
"""
|
|
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)
|
|
} |