""" Camera service for managing camera discovery and information """ from typing import List, Optional, Dict, Any from datetime import datetime import structlog from clients.sdk_bridge_client import sdk_bridge_client from clients.redis_client import redis_client from config import settings logger = structlog.get_logger() # Redis cache TTL for camera data (60 seconds) CAMERA_CACHE_TTL = 60 class CameraService: """Service for camera operations""" def __init__(self): """Initialize camera service""" pass async def list_cameras(self, use_cache: bool = True) -> Dict[str, Any]: """ Get list of all cameras from SDK Bridge Args: use_cache: Whether to use Redis cache (default: True) Returns: Dictionary with 'cameras' list and 'total' count """ cache_key = "cameras:list" # Try to get from cache first if use_cache: cached_data = await redis_client.get_json(cache_key) if cached_data: logger.info("camera_list_cache_hit") return cached_data logger.info("camera_list_cache_miss_fetching_from_sdk") try: # Fetch cameras from SDK Bridge via gRPC cameras = await sdk_bridge_client.list_cameras() # Transform to response format result = { "cameras": cameras, "total": len(cameras) } # Cache the result if use_cache: await redis_client.set_json(cache_key, result, expire=CAMERA_CACHE_TTL) logger.info("camera_list_cached", count=len(cameras), ttl=CAMERA_CACHE_TTL) return result except Exception as e: logger.error("camera_list_failed", error=str(e), exc_info=True) # Return empty list on error return {"cameras": [], "total": 0} async def get_camera(self, camera_id: int, use_cache: bool = True) -> Optional[Dict[str, Any]]: """ Get single camera by ID Args: camera_id: Camera ID (channel number) use_cache: Whether to use Redis cache (default: True) Returns: Camera dictionary or None if not found """ cache_key = f"cameras:detail:{camera_id}" # Try to get from cache first if use_cache: cached_data = await redis_client.get_json(cache_key) if cached_data: logger.info("camera_detail_cache_hit", camera_id=camera_id) return cached_data logger.info("camera_detail_cache_miss_fetching_from_sdk", camera_id=camera_id) try: # Fetch camera from SDK Bridge via gRPC camera = await sdk_bridge_client.get_camera(camera_id) if not camera: logger.warning("camera_not_found", camera_id=camera_id) return None # Cache the result if use_cache: await redis_client.set_json(cache_key, camera, expire=CAMERA_CACHE_TTL) logger.info("camera_detail_cached", camera_id=camera_id, ttl=CAMERA_CACHE_TTL) return camera except Exception as e: logger.error("camera_detail_failed", camera_id=camera_id, error=str(e), exc_info=True) return None async def invalidate_cache(self, camera_id: Optional[int] = None) -> None: """ Invalidate camera cache Args: camera_id: Specific camera ID to invalidate, or None to invalidate all """ if camera_id is not None: # Invalidate specific camera cache_key = f"cameras:detail:{camera_id}" await redis_client.delete(cache_key) logger.info("camera_cache_invalidated", camera_id=camera_id) else: # Invalidate camera list cache await redis_client.delete("cameras:list") logger.info("camera_list_cache_invalidated") async def refresh_camera_list(self) -> Dict[str, Any]: """ Force refresh camera list from SDK Bridge (bypass cache) Returns: Dictionary with 'cameras' list and 'total' count """ logger.info("camera_list_force_refresh") # Invalidate cache first await self.invalidate_cache() # Fetch fresh data return await self.list_cameras(use_cache=False) async def get_camera_count(self) -> int: """ Get total number of cameras Returns: Total camera count """ result = await self.list_cameras(use_cache=True) return result["total"] async def search_cameras(self, query: str) -> List[Dict[str, Any]]: """ Search cameras by name or description Args: query: Search query string Returns: List of matching cameras """ result = await self.list_cameras(use_cache=True) cameras = result["cameras"] # Simple case-insensitive search query_lower = query.lower() matching = [ cam for cam in cameras if query_lower in cam.get("name", "").lower() or query_lower in cam.get("description", "").lower() ] logger.info("camera_search", query=query, matches=len(matching)) return matching async def get_online_cameras(self) -> List[Dict[str, Any]]: """ Get list of online cameras only Returns: List of online cameras """ result = await self.list_cameras(use_cache=True) cameras = result["cameras"] online = [cam for cam in cameras if cam.get("status") == "online"] logger.info("online_cameras_retrieved", count=len(online), total=len(cameras)) return online async def get_ptz_cameras(self) -> List[Dict[str, Any]]: """ Get list of cameras with PTZ capabilities Returns: List of PTZ cameras """ result = await self.list_cameras(use_cache=True) cameras = result["cameras"] ptz_cameras = [cam for cam in cameras if cam.get("has_ptz", False)] logger.info("ptz_cameras_retrieved", count=len(ptz_cameras), total=len(cameras)) return ptz_cameras