Files
banban/talkingq-url/banban/routers/devices.py
2026-05-05 16:31:39 +08:00

316 lines
10 KiB
Python

import logging
from collections.abc import Mapping
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel, Field
from sqlalchemy import text
try:
from banban.security import get_current_user_id
from banban.schemas.location import (
DeviceLocationCurrentResponse,
DeviceLocationTrajectoryItem,
DeviceLocationTrajectoryResponse,
)
from banban.service.location import location_service
from banban.service.device import device_service
except ModuleNotFoundError:
from banban.security import get_current_user_id
from banban.schemas.location import (
DeviceLocationCurrentResponse,
DeviceLocationTrajectoryItem,
DeviceLocationTrajectoryResponse,
)
from banban.service.location import location_service
from banban.service.device import device_service
router = APIRouter(prefix="/devices", tags=["devices"])
logger = logging.getLogger("app.devices")
class DeviceMessageItem(BaseModel):
id: int
conversation_id: int
role_key: str
is_user: bool
speaker: str
content: str
timestamp: float
created_at: datetime
class DeviceMessageListResponse(BaseModel):
items: list[DeviceMessageItem]
total: int
next_cursor: int | None = None
class DeviceStatusResponse(BaseModel):
device_id: str
child_id: int | None = None
child_name: str | None = None
power: int | None = None
volume: int | None = None
signal: int | None = None
version: str | None = None
settings_updated_at: datetime | None = None
coord_type: str | None = None
lat: float | None = None
lng: float | None = None
accuracy_m: int | None = None
altitude_m: float | None = None
speed_mps: float | None = None
heading_deg: int | None = None
source: int | None = None
battery_pct: int | None = None
device_time: datetime | None = None
server_time: datetime | None = None
location_updated_at: datetime | None = None
class DeviceVolumeUpdateRequest(BaseModel):
level: int = Field(ge=0, le=100)
class DeviceVolumeUpdateResponse(BaseModel):
device_id: str
level: int
msg_id: str
def _row_to_message_item(row: Mapping) -> DeviceMessageItem:
is_user = bool(row["is_user"])
return DeviceMessageItem(
id=int(row["id"]),
conversation_id=int(row["conversation_id"]),
role_key=str(row["role_key"]),
is_user=is_user,
speaker="user" if is_user else "assistant",
content=str(row["content"]),
timestamp=float(row["timestamp"]),
created_at=row["created_at"],
)
def _row_to_current_location_response(row: Mapping) -> DeviceLocationCurrentResponse:
return DeviceLocationCurrentResponse(
child_id=int(row["child_id"]),
child_name=row.get("child_name"),
device_id=str(row["device_id"]),
coord_type=str(row["coord_type"]),
lat=float(row["lat"]),
lng=float(row["lng"]),
accuracy_m=row["accuracy_m"],
altitude_m=float(row["altitude_m"]) if row["altitude_m"] is not None else None,
speed_mps=float(row["speed_mps"]) if row["speed_mps"] is not None else None,
heading_deg=row["heading_deg"],
source=int(row["source"]),
battery_pct=row["battery_pct"],
device_time=row["device_time"],
server_time=row["server_time"],
updated_at=row["updated_at"],
)
def _row_to_trajectory_item(row: Mapping, *, child_name: str | None) -> DeviceLocationTrajectoryItem:
return DeviceLocationTrajectoryItem(
id=int(row["id"]),
child_id=int(row["child_id"]),
child_name=child_name,
device_id=str(row["device_id"]),
coord_type=str(row["coord_type"]),
lat=float(row["lat"]),
lng=float(row["lng"]),
accuracy_m=row["accuracy_m"],
altitude_m=float(row["altitude_m"]) if row["altitude_m"] is not None else None,
speed_mps=float(row["speed_mps"]) if row["speed_mps"] is not None else None,
heading_deg=row["heading_deg"],
source=int(row["source"]),
battery_pct=row["battery_pct"],
device_time=row["device_time"],
server_time=row["server_time"],
created_at=row["created_at"],
)
def _row_to_device_status_response(row: Mapping) -> DeviceStatusResponse:
return DeviceStatusResponse(
device_id=str(row["device_id"]),
child_id=int(row["child_id"]) if row["child_id"] is not None else None,
child_name=row.get("child_name"),
power=row["power"],
volume=row["volume"],
signal=row["signal_strength"],
version=row["version"],
settings_updated_at=row["settings_updated_at"],
coord_type=row["coord_type"],
lat=float(row["lat"]) if row["lat"] is not None else None,
lng=float(row["lng"]) if row["lng"] is not None else None,
accuracy_m=row["accuracy_m"],
altitude_m=float(row["altitude_m"]) if row["altitude_m"] is not None else None,
speed_mps=float(row["speed_mps"]) if row["speed_mps"] is not None else None,
heading_deg=row["heading_deg"],
source=int(row["source"]) if row["source"] is not None else None,
battery_pct=row["battery_pct"],
device_time=row["device_time"],
server_time=row["server_time"],
location_updated_at=row["location_updated_at"],
)
@router.get("/{device_id}/messages", response_model=DeviceMessageListResponse)
async def list_device_messages(
device_id: str,
request: Request,
cursor: int | None = Query(default=None, ge=1),
limit: int = Query(default=20, ge=1, le=100),
current_user_id: int = Depends(get_current_user_id),
) -> DeviceMessageListResponse:
rows = await device_service.list_device_messages(
device_id=device_id,
user_id=current_user_id,
cursor=cursor,
limit=limit,
)
has_more = len(rows) > limit
rows = rows[:limit]
next_cursor = int(rows[-1]["id"]) if has_more and rows else None
logger.info(
"listed device ai messages",
extra={
"event": "device_messages",
"request_id": getattr(request.state, "request_id", None),
"user_id": current_user_id,
"device_id": device_id,
"returned_count": len(rows),
},
)
return DeviceMessageListResponse(
items=[_row_to_message_item(row) for row in rows],
total=len(rows),
next_cursor=next_cursor,
)
@router.get("/{device_id}/status", response_model=DeviceStatusResponse)
async def get_device_status(
device_id: str,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> DeviceStatusResponse:
row = await device_service.get_device_status(
device_id=device_id,
user_id=current_user_id,
)
logger.info(
"device status fetched",
extra={
"event": "device_status",
"request_id": getattr(request.state, "request_id", None),
"user_id": current_user_id,
"device_id": device_id,
"child_id": row["child_id"],
},
)
return _row_to_device_status_response(row)
@router.post("/{device_id}/volume", response_model=DeviceVolumeUpdateResponse)
async def set_device_volume(
device_id: str,
payload: DeviceVolumeUpdateRequest,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> DeviceVolumeUpdateResponse:
msg_id = await device_service.set_device_volume(
device_id=device_id,
user_id=current_user_id,
level=payload.level,
)
logger.info(
"device volume command sent",
extra={
"event": "device_volume_set",
"request_id": getattr(request.state, "request_id", None),
"user_id": current_user_id,
"device_id": device_id,
"level": payload.level,
"msg_id": msg_id,
},
)
return DeviceVolumeUpdateResponse(device_id=device_id, level=payload.level, msg_id=msg_id)
@router.get("/{device_id}/location", response_model=DeviceLocationCurrentResponse)
async def get_current_device_location(
device_id: str,
request: Request,
current_user_id: int = Depends(get_current_user_id),
) -> DeviceLocationCurrentResponse:
row = await location_service.get_device_current_location(
device_id=device_id,
user_id=current_user_id,
)
logger.info(
"device current location fetched",
extra={
"event": "device_current_location",
"request_id": getattr(request.state, "request_id", None),
"user_id": current_user_id,
"device_id": device_id,
"child_id": int(row["child_id"]),
},
)
return _row_to_current_location_response(row)
@router.get("/{device_id}/trajectory", response_model=DeviceLocationTrajectoryResponse)
async def get_device_location_trajectory(
device_id: str,
request: Request,
start_at: datetime | None = Query(default=None),
end_at: datetime | None = Query(default=None),
limit: int = Query(default=200, ge=1, le=1000),
current_user_id: int = Depends(get_current_user_id),
) -> DeviceLocationTrajectoryResponse:
if start_at and end_at and start_at > end_at:
raise HTTPException(status_code=422, detail="start_at must be earlier than end_at")
access, rows = await location_service.get_device_trajectory(
device_id=device_id,
user_id=current_user_id,
start_at=start_at,
end_at=end_at,
limit=limit,
)
logger.info(
"device trajectory fetched",
extra={
"event": "device_trajectory",
"request_id": getattr(request.state, "request_id", None),
"user_id": current_user_id,
"device_id": device_id,
"child_id": access.child_id,
"count": len(rows),
},
)
return DeviceLocationTrajectoryResponse(
items=[_row_to_trajectory_item(row, child_name=access.child_name) for row in rows],
total=len(rows),
start_at=start_at,
end_at=end_at,
)