""" Device Controller Service Routes device operations to the appropriate backend module: - SLMM for sound level meters - SFM for seismographs (future implementation) This abstraction allows Projects system to work with any device type without knowing the underlying communication protocol. """ from typing import Dict, Any, Optional, List from backend.services.slmm_client import get_slmm_client, SLMMClientError class DeviceControllerError(Exception): """Base exception for device controller errors.""" pass class UnsupportedDeviceTypeError(DeviceControllerError): """Raised when device type is not supported.""" pass class DeviceController: """ Unified interface for controlling all device types. Routes commands to appropriate backend module based on device_type. Usage: controller = DeviceController() await controller.start_recording("nl43-001", "sound_level_meter", config={}) await controller.stop_recording("seismo-042", "seismograph") """ def __init__(self): self.slmm_client = get_slmm_client() # ======================================================================== # Recording Control # ======================================================================== async def start_recording( self, unit_id: str, device_type: str, config: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """ Start recording on a device. Args: unit_id: Unit identifier device_type: "sound_level_meter" | "seismograph" config: Device-specific recording configuration Returns: Response dict from device module Raises: UnsupportedDeviceTypeError: Device type not supported DeviceControllerError: Operation failed """ if device_type == "sound_level_meter": try: return await self.slmm_client.start_recording(unit_id, config) except SLMMClientError as e: raise DeviceControllerError(f"SLMM error: {str(e)}") elif device_type == "seismograph": # TODO: Implement SFM client for seismograph control # For now, return a placeholder response return { "status": "not_implemented", "message": "Seismograph recording control not yet implemented", "unit_id": unit_id, } else: raise UnsupportedDeviceTypeError( f"Device type '{device_type}' is not supported. " f"Supported types: sound_level_meter, seismograph" ) async def stop_recording( self, unit_id: str, device_type: str, ) -> Dict[str, Any]: """ Stop recording on a device. Args: unit_id: Unit identifier device_type: "sound_level_meter" | "seismograph" Returns: Response dict from device module """ if device_type == "sound_level_meter": try: return await self.slmm_client.stop_recording(unit_id) except SLMMClientError as e: raise DeviceControllerError(f"SLMM error: {str(e)}") elif device_type == "seismograph": # TODO: Implement SFM client return { "status": "not_implemented", "message": "Seismograph recording control not yet implemented", "unit_id": unit_id, } else: raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}") async def pause_recording( self, unit_id: str, device_type: str, ) -> Dict[str, Any]: """ Pause recording on a device. Args: unit_id: Unit identifier device_type: "sound_level_meter" | "seismograph" Returns: Response dict from device module """ if device_type == "sound_level_meter": try: return await self.slmm_client.pause_recording(unit_id) except SLMMClientError as e: raise DeviceControllerError(f"SLMM error: {str(e)}") elif device_type == "seismograph": return { "status": "not_implemented", "message": "Seismograph pause not yet implemented", "unit_id": unit_id, } else: raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}") async def resume_recording( self, unit_id: str, device_type: str, ) -> Dict[str, Any]: """ Resume paused recording on a device. Args: unit_id: Unit identifier device_type: "sound_level_meter" | "seismograph" Returns: Response dict from device module """ if device_type == "sound_level_meter": try: return await self.slmm_client.resume_recording(unit_id) except SLMMClientError as e: raise DeviceControllerError(f"SLMM error: {str(e)}") elif device_type == "seismograph": return { "status": "not_implemented", "message": "Seismograph resume not yet implemented", "unit_id": unit_id, } else: raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}") # ======================================================================== # Status & Monitoring # ======================================================================== async def get_device_status( self, unit_id: str, device_type: str, ) -> Dict[str, Any]: """ Get current device status. Args: unit_id: Unit identifier device_type: "sound_level_meter" | "seismograph" Returns: Status dict from device module """ if device_type == "sound_level_meter": try: return await self.slmm_client.get_unit_status(unit_id) except SLMMClientError as e: raise DeviceControllerError(f"SLMM error: {str(e)}") elif device_type == "seismograph": # TODO: Implement SFM status check return { "status": "not_implemented", "message": "Seismograph status not yet implemented", "unit_id": unit_id, } else: raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}") async def get_live_data( self, unit_id: str, device_type: str, ) -> Dict[str, Any]: """ Get live data from device. Args: unit_id: Unit identifier device_type: "sound_level_meter" | "seismograph" Returns: Live data dict from device module """ if device_type == "sound_level_meter": try: return await self.slmm_client.get_live_data(unit_id) except SLMMClientError as e: raise DeviceControllerError(f"SLMM error: {str(e)}") elif device_type == "seismograph": return { "status": "not_implemented", "message": "Seismograph live data not yet implemented", "unit_id": unit_id, } else: raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}") # ======================================================================== # Data Download # ======================================================================== async def download_files( self, unit_id: str, device_type: str, destination_path: str, files: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Download data files from device. Args: unit_id: Unit identifier device_type: "sound_level_meter" | "seismograph" destination_path: Local path to save files files: List of filenames, or None for all Returns: Download result with file list """ if device_type == "sound_level_meter": try: return await self.slmm_client.download_files( unit_id, destination_path, files, ) except SLMMClientError as e: raise DeviceControllerError(f"SLMM error: {str(e)}") elif device_type == "seismograph": # TODO: Implement SFM file download return { "status": "not_implemented", "message": "Seismograph file download not yet implemented", "unit_id": unit_id, } else: raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}") # ======================================================================== # Device Configuration # ======================================================================== async def update_device_config( self, unit_id: str, device_type: str, config: Dict[str, Any], ) -> Dict[str, Any]: """ Update device configuration. Args: unit_id: Unit identifier device_type: "sound_level_meter" | "seismograph" config: Configuration parameters Returns: Updated config from device module """ if device_type == "sound_level_meter": try: return await self.slmm_client.update_unit_config( unit_id, host=config.get("host"), tcp_port=config.get("tcp_port"), ftp_port=config.get("ftp_port"), ftp_username=config.get("ftp_username"), ftp_password=config.get("ftp_password"), ) except SLMMClientError as e: raise DeviceControllerError(f"SLMM error: {str(e)}") elif device_type == "seismograph": return { "status": "not_implemented", "message": "Seismograph config update not yet implemented", "unit_id": unit_id, } else: raise UnsupportedDeviceTypeError(f"Unsupported device type: {device_type}") # ======================================================================== # Health Check # ======================================================================== async def check_device_connectivity( self, unit_id: str, device_type: str, ) -> bool: """ Check if device is reachable. Args: unit_id: Unit identifier device_type: "sound_level_meter" | "seismograph" Returns: True if device is reachable, False otherwise """ if device_type == "sound_level_meter": try: status = await self.slmm_client.get_unit_status(unit_id) return status.get("last_seen") is not None except: return False elif device_type == "seismograph": # TODO: Implement SFM connectivity check return False else: return False # Singleton instance _default_controller: Optional[DeviceController] = None def get_device_controller() -> DeviceController: """ Get the default device controller instance. Returns: DeviceController instance """ global _default_controller if _default_controller is None: _default_controller = DeviceController() return _default_controller