""" Commands Router - 指令管理接口 API endpoints for sending commands / messages to devices and viewing command history. """ import math from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.schemas import ( APIResponse, CommandResponse, PaginatedList, ) from app.services import command_service, device_service router = APIRouter(prefix="/api/commands", tags=["Commands / 指令管理"]) # --------------------------------------------------------------------------- # Request schemas specific to this router # --------------------------------------------------------------------------- class SendCommandRequest(BaseModel): """Request body for sending a command to a device.""" device_id: int | None = Field(None, description="设备ID / Device ID (provide device_id or imei)") imei: str | None = Field(None, description="IMEI号 / IMEI number (provide device_id or imei)") command_type: str = Field(..., max_length=30, description="指令类型 / Command type") command_content: str = Field(..., description="指令内容 / Command content") class SendMessageRequest(BaseModel): """Request body for sending a message (0x82) to a device.""" device_id: int | None = Field(None, description="设备ID / Device ID (provide device_id or imei)") imei: str | None = Field(None, description="IMEI号 / IMEI number (provide device_id or imei)") message: str = Field(..., max_length=500, description="消息内容 / Message content") class SendTTSRequest(BaseModel): """Request body for sending a TTS voice broadcast to a device.""" device_id: int | None = Field(None, description="设备ID / Device ID (provide device_id or imei)") imei: str | None = Field(None, description="IMEI号 / IMEI number (provide device_id or imei)") text: str = Field(..., min_length=1, max_length=200, description="语音播报文本 / TTS text content") # --------------------------------------------------------------------------- # Helper # --------------------------------------------------------------------------- async def _resolve_device( db: AsyncSession, device_id: int | None, imei: str | None, ): """Resolve a device from either device_id or imei. Returns the Device ORM instance.""" if device_id is None and imei is None: raise HTTPException( status_code=400, detail="Either device_id or imei must be provided / 必须提供 device_id 或 imei", ) if device_id is not None: device = await device_service.get_device(db, device_id) else: device = await device_service.get_device_by_imei(db, imei) if device is None: identifier = f"ID={device_id}" if device_id else f"IMEI={imei}" raise HTTPException( status_code=404, detail=f"Device {identifier} not found / 未找到设备 {identifier}", ) return device # --------------------------------------------------------------------------- # Endpoints # --------------------------------------------------------------------------- @router.get( "", response_model=APIResponse[PaginatedList[CommandResponse]], summary="获取指令历史 / List command history", ) async def list_commands( device_id: int | None = Query(default=None, description="设备ID / Device ID"), status: str | None = Query(default=None, description="指令状态 / Command status (pending/sent/success/failed)"), page: int = Query(default=1, ge=1, description="页码 / Page number"), page_size: int = Query(default=20, ge=1, le=100, description="每页数量 / Items per page"), db: AsyncSession = Depends(get_db), ): """ 获取指令历史记录,支持按设备和状态过滤。 List command history with optional device and status filters. """ commands, total = await command_service.get_commands( db, device_id=device_id, status=status, page=page, page_size=page_size ) return APIResponse( data=PaginatedList( items=[CommandResponse.model_validate(c) for c in commands], total=total, page=page, page_size=page_size, total_pages=math.ceil(total / page_size) if total else 0, ) ) @router.post( "/send", response_model=APIResponse[CommandResponse], status_code=201, summary="发送指令 / Send command to device", ) async def send_command(body: SendCommandRequest, db: AsyncSession = Depends(get_db)): """ 向设备发送指令(通过TCP连接下发)。 Send a command to a device via the TCP connection. Requires the device to be online. """ device = await _resolve_device(db, body.device_id, body.imei) # Import tcp_manager lazily to avoid circular imports from app.tcp_server import tcp_manager # Check if device is connected if device.imei not in tcp_manager.connections: raise HTTPException( status_code=400, detail=f"Device {device.imei} is not online / 设备 {device.imei} 不在线", ) # Create command log entry command_log = await command_service.create_command( db, device_id=device.id, command_type=body.command_type, command_content=body.command_content, ) # Send command via TCP try: await tcp_manager.send_command( device.imei, body.command_type, body.command_content ) except Exception as e: command_log.status = "failed" await db.flush() await db.refresh(command_log) raise HTTPException( status_code=500, detail=f"Failed to send command / 指令发送失败: {str(e)}", ) command_log.status = "sent" await db.flush() await db.refresh(command_log) return APIResponse( message="Command sent successfully / 指令发送成功", data=CommandResponse.model_validate(command_log), ) @router.post( "/message", response_model=APIResponse[CommandResponse], status_code=201, summary="发送留言 / Send message to device (0x82)", ) async def send_message(body: SendMessageRequest, db: AsyncSession = Depends(get_db)): """ 向设备发送留言消息(协议号 0x82)。 Send a text message to a device using protocol 0x82. """ device = await _resolve_device(db, body.device_id, body.imei) from app.tcp_server import tcp_manager if device.imei not in tcp_manager.connections: raise HTTPException( status_code=400, detail=f"Device {device.imei} is not online / 设备 {device.imei} 不在线", ) # Create command log for the message command_log = await command_service.create_command( db, device_id=device.id, command_type="message", command_content=body.message, ) try: await tcp_manager.send_message(device.imei, body.message) except Exception as e: command_log.status = "failed" await db.flush() await db.refresh(command_log) raise HTTPException( status_code=500, detail=f"Failed to send message / 留言发送失败: {str(e)}", ) command_log.status = "sent" await db.flush() await db.refresh(command_log) return APIResponse( message="Message sent successfully / 留言发送成功", data=CommandResponse.model_validate(command_log), ) @router.post( "/tts", response_model=APIResponse[CommandResponse], status_code=201, summary="语音下发 / Send TTS voice broadcast to device", ) async def send_tts(body: SendTTSRequest, db: AsyncSession = Depends(get_db)): """ 向设备发送 TTS 语音播报(通过 0x80 在线指令,TTS 命令格式)。 Send a TTS voice broadcast to a device via online command (0x80). The device will use its built-in TTS engine to speak the text aloud. """ device = await _resolve_device(db, body.device_id, body.imei) from app.tcp_server import tcp_manager if device.imei not in tcp_manager.connections: raise HTTPException( status_code=400, detail=f"Device {device.imei} is not online / 设备 {device.imei} 不在线", ) tts_command = f"TTS,{body.text}" # Create command log entry command_log = await command_service.create_command( db, device_id=device.id, command_type="tts", command_content=tts_command, ) try: await tcp_manager.send_command(device.imei, "tts", tts_command) except Exception as e: command_log.status = "failed" await db.flush() await db.refresh(command_log) raise HTTPException( status_code=500, detail=f"Failed to send TTS / 语音下发失败: {str(e)}", ) command_log.status = "sent" await db.flush() await db.refresh(command_log) return APIResponse( message="TTS sent successfully / 语音下发成功", data=CommandResponse.model_validate(command_log), ) @router.get( "/{command_id}", response_model=APIResponse[CommandResponse], summary="获取指令详情 / Get command details", ) async def get_command(command_id: int, db: AsyncSession = Depends(get_db)): """ 按ID获取指令详情。 Get command log details by ID. """ command = await command_service.get_command(db, command_id) if command is None: raise HTTPException(status_code=404, detail=f"Command {command_id} not found / 未找到指令{command_id}") return APIResponse(data=CommandResponse.model_validate(command))