feat(mqtt): add device health telemetry and migrate to v2 topic spec
Two efforts that landed together because the v2 topic work extends tables the health-telemetry effort added days earlier in the same files/functions, making them impractical to separate cleanly: Health/diagnostics telemetry (schema, Jul 13-17): - New Postgres tables: device_alert_events, device_boot_events, device_ping_samples, device_diagnostics_reports, plus a `source` column on device_logs to distinguish log origins - Query/service layer in pg_mqtt.py and database/__init__.py for inserting and listing this history, plus a "latest metrics" endpoint combining most-recent diagnostics + ping RTT per device - mqtt/router.py gains list endpoints for alert/boot/ping/diagnostics history, consumed by the upcoming Health tab MQTT v2 topic migration (Sep 21): - Heartbeat payload flattened per vesper_mqtt_topic_spec_v2.md, adding rssi/free_heap/state/ok fields - Command replies move to control/ack, device-initiated events to control/reports; mqtt/client.py subscribes to the new topic set and runs a ping_loop (wired up in main.py) for RTT sampling - mqtt/logger.py and pg_mqtt.py updated to parse and persist the new payload shape alongside the legacy fields for backwards compatibility Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ class DeviceLogEntry(BaseModel):
|
||||
level: str
|
||||
message: str
|
||||
device_timestamp: Optional[int] = None
|
||||
source: str = "log"
|
||||
received_at: str
|
||||
|
||||
|
||||
@@ -39,6 +40,10 @@ class HeartbeatEntry(BaseModel):
|
||||
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
|
||||
|
||||
|
||||
@@ -53,11 +58,21 @@ class CommandEntry(BaseModel):
|
||||
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):
|
||||
@@ -96,3 +111,99 @@ class DeviceAlertEntry(BaseModel):
|
||||
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user