first commit

This commit is contained in:
rafaeldpsilva
2025-09-09 13:46:42 +01:00
commit a7a18e6295
77 changed files with 8678 additions and 0 deletions

View File

@@ -0,0 +1,328 @@
"""
Sensor business logic service
Business Layer - handles sensor-related business operations and rules
"""
import json
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
import logging
import uuid
from models import (
SensorReading, LegacySensorReading, SensorMetadata,
SensorType, SensorStatus, CO2Status, OccupancyLevel
)
from ..infrastructure.repositories import (
SensorReadingRepository, SensorMetadataRepository,
SystemEventRepository, RedisRepository
)
logger = logging.getLogger(__name__)
class SensorService:
"""Service for sensor-related business operations"""
def __init__(self):
self.sensor_reading_repo = SensorReadingRepository()
self.sensor_metadata_repo = SensorMetadataRepository()
self.system_event_repo = SystemEventRepository()
self.redis_repo = RedisRepository()
async def process_sensor_message(self, message_data: str) -> bool:
"""Process incoming sensor message and handle business logic"""
try:
# Parse the message
data = json.loads(message_data)
logger.debug(f"Processing sensor message: {data}")
# Convert to standard format
sensor_reading = await self._parse_sensor_data(data)
# Validate business rules
validation_result = await self._validate_sensor_reading(sensor_reading)
if not validation_result["valid"]:
logger.warning(f"Sensor reading validation failed: {validation_result['errors']}")
return False
# Store the reading
stored = await self.sensor_reading_repo.create(sensor_reading)
if not stored:
return False
# Update caches and metadata
await self._update_caches(sensor_reading)
await self._update_sensor_metadata(sensor_reading)
# Check for alerts
await self._check_sensor_alerts(sensor_reading)
return True
except Exception as e:
logger.error(f"Error processing sensor message: {e}")
await self._log_processing_error(str(e), message_data)
return False
async def _parse_sensor_data(self, data: dict) -> SensorReading:
"""Parse and convert sensor data to standard format"""
# Check if legacy format
if self._is_legacy_format(data):
return await self._convert_legacy_data(data)
else:
return SensorReading(**data)
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,
timestamp=legacy_reading.timestamp,
created_at=legacy_reading.created_at,
energy={
"value": legacy_reading.value,
"unit": legacy_reading.unit
}
)
async def _validate_sensor_reading(self, reading: SensorReading) -> Dict[str, Any]:
"""Validate sensor reading against business rules"""
errors = []
# Check timestamp is not too far in the future
future_threshold = datetime.utcnow().timestamp() + 3600 # 1 hour
if reading.timestamp > future_threshold:
errors.append("Timestamp is too far in the future")
# Check timestamp is not too old
past_threshold = datetime.utcnow().timestamp() - 86400 # 24 hours
if reading.timestamp < past_threshold:
errors.append("Timestamp is too old")
# Validate sensor values
if reading.energy:
energy_value = reading.energy.get("value", 0)
if energy_value < 0 or energy_value > 1000: # Reasonable energy range
errors.append("Energy value is out of acceptable range")
if reading.co2:
co2_value = reading.co2.get("value", 0)
if co2_value < 0 or co2_value > 50000: # Reasonable CO2 range
errors.append("CO2 value is out of acceptable range")
if reading.temperature:
temp_value = reading.temperature.get("value", 0)
if temp_value < -50 or temp_value > 100: # Reasonable temperature range
errors.append("Temperature value is out of acceptable range")
return {
"valid": len(errors) == 0,
"errors": errors
}
async def _update_caches(self, reading: SensorReading) -> None:
"""Update Redis caches with latest sensor data"""
# Cache latest sensor reading
await self.redis_repo.set_sensor_data(
reading.sensor_id,
reading.dict(),
expire_seconds=3600
)
# Update sensor status
status_data = {
"status": "online",
"last_seen": reading.timestamp,
"room": reading.room
}
await self.redis_repo.set_sensor_status(
reading.sensor_id,
status_data,
expire_seconds=1800
)
async def _update_sensor_metadata(self, reading: SensorReading) -> None:
"""Update or create sensor metadata"""
existing = await self.sensor_metadata_repo.get_by_sensor_id(reading.sensor_id)
if existing:
# Update existing metadata
updates = {
"last_seen": datetime.utcnow(),
"status": SensorStatus.ONLINE.value
}
# Add sensor type to monitoring capabilities if not present
capabilities = existing.get("monitoring_capabilities", [])
if reading.sensor_type.value not in capabilities:
capabilities.append(reading.sensor_type.value)
updates["monitoring_capabilities"] = capabilities
await self.sensor_metadata_repo.update(reading.sensor_id, updates)
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.sensor_metadata_repo.create(metadata)
logger.info(f"Created metadata for new sensor: {reading.sensor_id}")
async def _check_sensor_alerts(self, reading: SensorReading) -> None:
"""Check for alert conditions in sensor data"""
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:
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'}"
})
# Log alerts as system events
for alert in alerts:
await self._log_alert_event(reading, **alert)
async def _log_alert_event(self, reading: SensorReading, event_type: str, severity: str,
title: str, description: str) -> None:
"""Log an alert as a system event"""
from models import SystemEvent
event = SystemEvent(
event_id=str(uuid.uuid4()),
event_type=event_type,
severity=severity,
timestamp=int(datetime.utcnow().timestamp()),
title=title,
description=description,
sensor_id=reading.sensor_id,
room=reading.room,
source="sensor_service",
data=reading.dict()
)
await self.system_event_repo.create(event)
async def _log_processing_error(self, error_message: str, raw_data: str) -> None:
"""Log data processing error"""
from models import SystemEvent
event = SystemEvent(
event_id=str(uuid.uuid4()),
event_type="data_processing_error",
severity="error",
timestamp=int(datetime.utcnow().timestamp()),
title="Sensor Data Processing Failed",
description=f"Failed to process sensor message: {error_message}",
source="sensor_service",
data={"raw_message": raw_data}
)
await self.system_event_repo.create(event)
async def get_sensor_details(self, sensor_id: str) -> Optional[Dict[str, Any]]:
"""Get complete sensor details including metadata and recent readings"""
# Get metadata
metadata = await self.sensor_metadata_repo.get_by_sensor_id(sensor_id)
if not metadata:
return None
# Get recent readings
recent_readings = await self.sensor_reading_repo.get_recent_by_sensor(
sensor_id=sensor_id,
limit=100,
minutes=1440 # 24 hours
)
# Get latest reading from cache
latest_reading = await self.redis_repo.get_sensor_data(sensor_id)
return {
"sensor": metadata,
"latest_reading": latest_reading,
"recent_readings_count": len(recent_readings),
"recent_readings": recent_readings[:10] # Return only 10 most recent
}
async def update_sensor_metadata(self, sensor_id: str, metadata_updates: Dict[str, Any]) -> bool:
"""Update sensor metadata with business validation"""
# Validate updates
if "sensor_id" in metadata_updates:
del metadata_updates["sensor_id"] # Cannot change sensor ID
# Update timestamp
metadata_updates["updated_at"] = datetime.utcnow()
return await self.sensor_metadata_repo.update(sensor_id, metadata_updates)
async def delete_sensor(self, sensor_id: str) -> Dict[str, Any]:
"""Delete a sensor and all its associated data"""
# Delete readings
readings_deleted = await self.sensor_reading_repo.delete_by_sensor_id(sensor_id)
# Delete metadata
metadata_deleted = await self.sensor_metadata_repo.delete(sensor_id)
# Clear cache
await self.redis_repo.delete_sensor_cache(sensor_id)
return {
"sensor_id": sensor_id,
"readings_deleted": readings_deleted,
"metadata_deleted": metadata_deleted
}
async def get_all_sensors(self, filters: Dict[str, Any] = None) -> Dict[str, Any]:
"""Get all sensors with optional filtering"""
sensors = await self.sensor_metadata_repo.get_all(filters)
return {
"sensors": sensors,
"count": len(sensors),
"filters": filters or {}
}