Refactor: done phase 5

This commit is contained in:
ziesorx 2025-09-24 22:01:26 +07:00
parent 5176f99ba7
commit 476f19cabe
3 changed files with 740 additions and 109 deletions

View file

@ -0,0 +1,282 @@
"""
License Plate Manager Module.
Handles Redis subscription to license plate results from LPR service.
"""
import logging
import json
import asyncio
from typing import Dict, Optional, Any, Callable
import redis.asyncio as redis
logger = logging.getLogger(__name__)
class LicensePlateManager:
"""
Manages license plate result subscription from Redis channel.
Subscribes to 'license_results' channel for license plate data from LPR service.
"""
def __init__(self, redis_config: Dict[str, Any]):
"""
Initialize license plate manager with Redis configuration.
Args:
redis_config: Redis configuration dictionary
"""
self.config = redis_config
self.redis_client: Optional[redis.Redis] = None
self.pubsub = None
self.subscription_task = None
self.callback = None
# Connection parameters
self.host = redis_config.get('host', 'localhost')
self.port = redis_config.get('port', 6379)
self.password = redis_config.get('password')
self.db = redis_config.get('db', 0)
# License plate data cache - store recent results by session_id
self.license_plate_cache: Dict[str, Dict[str, Any]] = {}
self.cache_ttl = 300 # 5 minutes TTL for cached results
logger.info(f"LicensePlateManager initialized for {self.host}:{self.port}")
async def initialize(self, callback: Optional[Callable] = None) -> bool:
"""
Initialize Redis connection and start subscription to license_results channel.
Args:
callback: Optional callback function for processing license plate results
Returns:
True if successful, False otherwise
"""
try:
# Create Redis connection
self.redis_client = redis.Redis(
host=self.host,
port=self.port,
password=self.password,
db=self.db,
decode_responses=True
)
# Test connection
await self.redis_client.ping()
logger.info(f"Connected to Redis for license plate subscription")
# Set callback
self.callback = callback
# Start subscription
await self._start_subscription()
return True
except Exception as e:
logger.error(f"Failed to initialize license plate manager: {e}", exc_info=True)
return False
async def _start_subscription(self):
"""Start Redis subscription to license_results channel."""
try:
if not self.redis_client:
logger.error("Redis client not initialized")
return
# Create pubsub and subscribe
self.pubsub = self.redis_client.pubsub()
await self.pubsub.subscribe('license_results')
logger.info("Subscribed to Redis channel: license_results")
# Start listening task
self.subscription_task = asyncio.create_task(self._listen_for_messages())
except Exception as e:
logger.error(f"Error starting license plate subscription: {e}", exc_info=True)
async def _listen_for_messages(self):
"""Listen for messages on the license_results channel."""
try:
if not self.pubsub:
return
async for message in self.pubsub.listen():
if message['type'] == 'message':
try:
# Log the raw message from Redis channel
logger.info(f"[LICENSE PLATE RAW] Received from 'license_results' channel: {message['data']}")
# Parse the license plate result message
data = json.loads(message['data'])
logger.info(f"[LICENSE PLATE PARSED] Parsed JSON data: {data}")
await self._process_license_plate_result(data)
except json.JSONDecodeError as e:
logger.error(f"[LICENSE PLATE ERROR] Invalid JSON in license plate message: {e}")
logger.error(f"[LICENSE PLATE ERROR] Raw message was: {message['data']}")
except Exception as e:
logger.error(f"Error processing license plate message: {e}", exc_info=True)
except asyncio.CancelledError:
logger.info("License plate subscription task cancelled")
except Exception as e:
logger.error(f"Error in license plate message listener: {e}", exc_info=True)
async def _process_license_plate_result(self, data: Dict[str, Any]):
"""
Process incoming license plate result from LPR service.
Expected message format (from actual LPR service):
{
"session_id": "511",
"license_character": "ข3184"
}
or
{
"session_id": "508",
"display_id": "test3",
"license_plate_text": "ABC-123",
"confidence": 0.95,
"timestamp": "2025-09-24T21:10:00Z"
}
Args:
data: License plate result data
"""
try:
session_id = data.get('session_id')
if not session_id:
logger.warning("License plate result missing session_id")
return
# Handle different message formats
# Format 1: {"session_id": "511", "license_character": "ข3184"}
# Format 2: {"session_id": "508", "license_plate_text": "ABC-123", "confidence": 0.95, ...}
license_plate_text = data.get('license_plate_text') or data.get('license_character')
confidence = data.get('confidence', 1.0) # Default confidence for LPR service results
display_id = data.get('display_id', '')
timestamp = data.get('timestamp', '')
logger.info(f"[LICENSE PLATE] Received result for session {session_id}: "
f"text='{license_plate_text}', confidence={confidence:.3f}")
# Store in cache
self.license_plate_cache[session_id] = {
'license_plate_text': license_plate_text,
'confidence': confidence,
'display_id': display_id,
'timestamp': timestamp,
'received_at': asyncio.get_event_loop().time()
}
# Call callback if provided
if self.callback:
await self.callback(session_id, {
'license_plate_text': license_plate_text,
'confidence': confidence,
'display_id': display_id,
'timestamp': timestamp
})
except Exception as e:
logger.error(f"Error processing license plate result: {e}", exc_info=True)
def get_license_plate_result(self, session_id: str) -> Optional[Dict[str, Any]]:
"""
Get cached license plate result for a session.
Args:
session_id: Session identifier
Returns:
License plate result dictionary or None if not found
"""
if session_id not in self.license_plate_cache:
return None
result = self.license_plate_cache[session_id]
# Check TTL
current_time = asyncio.get_event_loop().time()
if current_time - result.get('received_at', 0) > self.cache_ttl:
# Expired, remove from cache
del self.license_plate_cache[session_id]
return None
return {
'license_plate_text': result.get('license_plate_text'),
'confidence': result.get('confidence'),
'display_id': result.get('display_id'),
'timestamp': result.get('timestamp')
}
def cleanup_expired_results(self):
"""Remove expired license plate results from cache."""
try:
current_time = asyncio.get_event_loop().time()
expired_sessions = []
for session_id, result in self.license_plate_cache.items():
if current_time - result.get('received_at', 0) > self.cache_ttl:
expired_sessions.append(session_id)
for session_id in expired_sessions:
del self.license_plate_cache[session_id]
logger.debug(f"Removed expired license plate result for session {session_id}")
except Exception as e:
logger.error(f"Error cleaning up expired license plate results: {e}", exc_info=True)
async def close(self):
"""Close Redis connection and cleanup resources."""
try:
# Cancel subscription task first
if self.subscription_task and not self.subscription_task.done():
self.subscription_task.cancel()
try:
await self.subscription_task
except asyncio.CancelledError:
logger.debug("License plate subscription task cancelled successfully")
except Exception as e:
logger.warning(f"Error waiting for subscription task cancellation: {e}")
# Close pubsub connection properly
if self.pubsub:
try:
# First unsubscribe from channels
await self.pubsub.unsubscribe('license_results')
# Then close the pubsub connection
await self.pubsub.aclose()
except Exception as e:
logger.warning(f"Error closing pubsub connection: {e}")
finally:
self.pubsub = None
# Close Redis connection
if self.redis_client:
try:
await self.redis_client.aclose()
except Exception as e:
logger.warning(f"Error closing Redis connection: {e}")
finally:
self.redis_client = None
# Clear cache
self.license_plate_cache.clear()
logger.info("License plate manager closed successfully")
except Exception as e:
logger.error(f"Error closing license plate manager: {e}", exc_info=True)
def get_statistics(self) -> Dict[str, Any]:
"""Get license plate manager statistics."""
return {
'cached_results': len(self.license_plate_cache),
'connected': self.redis_client is not None,
'subscribed': self.pubsub is not None,
'host': self.host,
'port': self.port
}