first commit
This commit is contained in:
300
layers/business/analytics_service.py
Normal file
300
layers/business/analytics_service.py
Normal file
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user