import json from datetime import datetime from typing import Literal from pydantic import BaseModel, Field, field_validator # --- Device schemas --- class DeviceRegisterRequest(BaseModel): device_id: str = Field(..., min_length=1, max_length=50) hostname: str = Field(..., min_length=1, max_length=100) ip_address: str = Field(..., min_length=1, max_length=45) firmware_version: str | None = Field(None, max_length=20) has_nfc: bool = True has_scale: bool = True tare_offset: int = 0 calibration_factor: float = 1.0 nfc_reader_type: str | None = Field(None, max_length=20) nfc_connection: str | None = Field(None, max_length=20) backend_url: str | None = Field(None, max_length=255) has_backlight: bool = False class DeviceResponse(BaseModel): id: int device_id: str hostname: str ip_address: str firmware_version: str | None = None has_nfc: bool has_scale: bool tare_offset: int calibration_factor: float nfc_reader_type: str | None = None nfc_connection: str | None = None backend_url: str | None = None display_brightness: int = 100 display_blank_timeout: int = 0 has_backlight: bool = False last_calibrated_at: datetime | None = None last_seen: datetime | None = None pending_command: str | None = None nfc_ok: bool scale_ok: bool uptime_s: int update_status: str | None = None update_message: str | None = None system_stats: dict | None = None online: bool = False ssh_public_key: str | None = None created_at: datetime updated_at: datetime class Config: from_attributes = True class HeartbeatRequest(BaseModel): nfc_ok: bool = False scale_ok: bool = False uptime_s: int = 0 firmware_version: str | None = Field(None, max_length=20) ip_address: str | None = Field(None, max_length=45) nfc_reader_type: str | None = Field(None, max_length=20) nfc_connection: str | None = Field(None, max_length=20) backend_url: str | None = Field(None, max_length=255) system_stats: dict | None = None @field_validator("system_stats") @classmethod def _limit_system_stats_size(cls, v: dict | None) -> dict | None: if v is not None and len(json.dumps(v)) > 4096: raise ValueError("system_stats must not exceed 4096 bytes when JSON-encoded") return v class HeartbeatResponse(BaseModel): pending_command: str | None = None pending_write_payload: dict | None = None pending_system_payload: dict | None = None tare_offset: int calibration_factor: float display_brightness: int = 100 display_blank_timeout: int = 0 ssh_public_key: str | None = None # --- NFC schemas --- class TagScannedRequest(BaseModel): device_id: str = Field(..., max_length=50) tag_uid: str = Field(..., max_length=32) tray_uuid: str | None = Field(None, max_length=32, pattern=r"^[0-9A-Fa-f]*$") sak: int | None = None tag_type: str | None = Field(None, max_length=50) raw_blocks: dict | None = None class TagRemovedRequest(BaseModel): device_id: str = Field(..., max_length=50) tag_uid: str = Field(..., max_length=32) # --- Scale schemas --- class ScaleReadingRequest(BaseModel): device_id: str = Field(..., max_length=50) weight_grams: float = Field(..., allow_inf_nan=False) stable: bool = False raw_adc: int | None = None class UpdateSpoolWeightRequest(BaseModel): spool_id: int = Field(..., gt=0) weight_grams: float = Field(..., allow_inf_nan=False, ge=0.0, le=100_000.0) # --- Calibration schemas --- class SetTareRequest(BaseModel): tare_offset: int class SetCalibrationFactorRequest(BaseModel): known_weight_grams: float = Field(..., gt=0) raw_adc: int tare_raw_adc: int | None = None class CalibrationResponse(BaseModel): tare_offset: int calibration_factor: float # --- Display schemas --- class WriteTagRequest(BaseModel): device_id: str = Field(..., max_length=50) spool_id: int = Field(..., gt=0) class WriteTagResultRequest(BaseModel): device_id: str = Field(..., max_length=50) spool_id: int = Field(..., gt=0) tag_uid: str = Field(..., min_length=8, max_length=30, pattern=r"^[0-9A-Fa-f]+$") success: bool message: str | None = Field(None, max_length=500) class DisplaySettingsRequest(BaseModel): brightness: int = Field(ge=0, le=100) blank_timeout: int = Field(ge=0) class SystemConfigRequest(BaseModel): backend_url: str = Field(..., min_length=1, max_length=255) api_key: str | None = Field(default=None, max_length=255) class SystemCommandRequest(BaseModel): command: str = Field( ..., max_length=50, description="System command: reboot, shutdown, restart_daemon, restart_browser" ) class SystemCommandResultRequest(BaseModel): command: str = Field(..., max_length=50) success: bool message: str | None = Field(None, max_length=500) class UpdateStatusRequest(BaseModel): status: Literal["updating", "complete", "error"] message: str | None = Field(None, max_length=255) # --- Diagnostics schemas --- class DiagnosticResultRequest(BaseModel): diagnostic: str = Field(..., max_length=50, description="Diagnostic type: 'nfc', 'scale', or 'read_tag'") success: bool output: str = Field(..., max_length=10_000) exit_code: int = Field(..., ge=-255, le=255)