from pydantic import BaseModel, Field from typing import Optional, List, Any, Dict from enum import Enum class LogLevel(str, Enum): INFO = "INFO" WARN = "WARN" ERROR = "ERROR" class CommandStatus(str, Enum): PENDING = "pending" SUCCESS = "success" ERROR = "error" TIMEOUT = "timeout" class MqttCommandRequest(BaseModel): cmd: str = Field(..., description="Command name: ping, playback, file_manager, relay_setup, clock_setup, system_info, system") contents: Dict[str, Any] = Field(default_factory=dict, description="Command payload contents") class DeviceLogEntry(BaseModel): id: int device_serial: str level: str message: str device_timestamp: Optional[int] = None source: str = "log" received_at: str class HeartbeatEntry(BaseModel): id: int device_serial: str device_id: Optional[str] = None firmware_version: Optional[str] = None ip_address: Optional[str] = None gateway: Optional[str] = None uptime_ms: Optional[int] = None uptime_display: Optional[str] = None rssi: Optional[int] = None free_heap: Optional[int] = None state: Optional[str] = None # "idle" | "playing" | "paused" | "error" | "booting" (v2 firmware only) ok: Optional[bool] = None # overall device health, independent of playback state (v2 firmware only) received_at: str class CommandEntry(BaseModel): id: int device_serial: str command_name: str command_payload: Optional[str] = None status: str response_payload: Optional[str] = None sent_at: str responded_at: Optional[str] = None class AlertEventEntry(BaseModel): id: int device_serial: str subsystem: str state: str message: Optional[str] = None occurred_at: str class DeviceMqttStatus(BaseModel): device_serial: str online: bool last_heartbeat: Optional[HeartbeatEntry] = None seconds_since_heartbeat: Optional[int] = None last_alert_event: Optional[AlertEventEntry] = None class MqttStatusResponse(BaseModel): devices: List[DeviceMqttStatus] broker_connected: bool = False class LogListResponse(BaseModel): logs: List[DeviceLogEntry] total: int class HeartbeatListResponse(BaseModel): heartbeats: List[HeartbeatEntry] total: int class CommandListResponse(BaseModel): commands: List[CommandEntry] total: int class CommandSendResponse(BaseModel): success: bool command_id: int message: str class DeviceAlertEntry(BaseModel): device_serial: str subsystem: str state: str message: Optional[str] = None updated_at: str class DeviceAlertsResponse(BaseModel): alerts: List[DeviceAlertEntry] class AlertEventListResponse(BaseModel): events: List[AlertEventEntry] total: int class BootEventEntry(BaseModel): id: int device_serial: str boot_count: Optional[int] = None reset_reason: Optional[str] = None is_fault: bool = False free_heap: Optional[int] = None crash_task: Optional[str] = None crash_pc: Optional[int] = None crash_exc_cause: Optional[int] = None crash_exc_vaddr: Optional[int] = None occurred_at: str class BootEventListResponse(BaseModel): events: List[BootEventEntry] total: int class PingSampleEntry(BaseModel): id: int device_serial: str rtt_ms: int sampled_at: str class PingSampleListResponse(BaseModel): samples: List[PingSampleEntry] total: int class DiagnosticsReportEntry(BaseModel): id: int device_serial: str cpu_temp_avg: Optional[float] = None cpu_temp_min: Optional[float] = None cpu_temp_max: Optional[float] = None cpu_temp_samples: Optional[int] = None wifi_reconnect_count: Optional[int] = None wifi_last_disconnect_reason: Optional[str] = None wifi_last_disconnect_uptime_ms: Optional[int] = None ota_current_version: Optional[str] = None ota_update_available: Optional[bool] = None ota_available_version: Optional[str] = None ota_last_check_uptime_ms: Optional[int] = None ota_last_error: Optional[str] = None stack_high_water: Optional[str] = None # JSON-encoded string — see insert_diagnostics_report bell_strikes: Optional[str] = None # JSON-encoded string, {"0": count, ...} — v2 firmware only bell_loads: Optional[str] = None # JSON-encoded string, {"0": load, ...} — v2 firmware only cooling_active: Optional[bool] = None # v2 firmware only received_at: str class DiagnosticsReportListResponse(BaseModel): reports: List[DiagnosticsReportEntry] total: int class DeviceReportEntry(BaseModel): """A control/reports event (currently only bell_overload) — see project-vesper's vesper_mqtt_topic_spec_v2.md. Console-side storage is intentionally light: history/audit only, not a real-time UI surface.""" id: int device_serial: str report_type: str payload: Optional[str] = None # JSON-encoded string occurred_at: str class DeviceReportListResponse(BaseModel): reports: List[DeviceReportEntry] total: int class LatestDiagnosticsEntry(BaseModel): device_serial: str cpu_temp_avg: Optional[float] = None received_at: str class LatestPingEntry(BaseModel): device_serial: str rtt_ms: int sampled_at: str class LatestMetricsResponse(BaseModel): diagnostics: List[LatestDiagnosticsEntry] pings: List[LatestPingEntry]