"""Pydantic schemas for the Modbus device CRUD + readings + metrics API (M5-T05).""" from __future__ import annotations from datetime import datetime from typing import Any from pydantic import BaseModel, Field # --------------------------------------------------------------------------- # Device schemas # --------------------------------------------------------------------------- class ModbusDeviceCreate(BaseModel): """Request body for POST /api/modbus/devices.""" friendly_name: str = Field(..., min_length=1, max_length=255) transport: str = Field(default="tcp", max_length=16) host: str = Field(..., min_length=1, max_length=255) port: int = Field(default=502, ge=1, le=65535) unit_id: int = Field(default=1, ge=0, le=247) profile: str = Field(..., min_length=1, max_length=64) poll_interval_s: int = Field(default=5, ge=1, le=3600) enabled: bool = Field(default=True) class ModbusDeviceUpdate(BaseModel): """Request body for PATCH /api/modbus/devices/{uuid} — all fields optional.""" friendly_name: str | None = Field(default=None, min_length=1, max_length=255) transport: str | None = Field(default=None, max_length=16) host: str | None = Field(default=None, min_length=1, max_length=255) port: int | None = Field(default=None, ge=1, le=65535) unit_id: int | None = Field(default=None, ge=0, le=247) profile: str | None = Field(default=None, min_length=1, max_length=64) poll_interval_s: int | None = Field(default=None, ge=1, le=3600) enabled: bool | None = None class ModbusDeviceResponse(BaseModel): """Response schema for a single Modbus device.""" uuid: str friendly_name: str transport: str host: str port: int unit_id: int profile: str poll_interval_s: int enabled: bool last_poll_at: datetime | None last_poll_ok: bool | None created_at: datetime updated_at: datetime model_config = {"from_attributes": True} class ModbusDeviceListResponse(BaseModel): """Response schema for listing Modbus devices.""" items: list[ModbusDeviceResponse] total: int # --------------------------------------------------------------------------- # Reading schemas # --------------------------------------------------------------------------- class ModbusReadingResponse(BaseModel): """A single reading row: timestamp + decoded payload.""" recorded_at: datetime payload: dict[str, Any] model_config = {"from_attributes": True} class ModbusReadingsResponse(BaseModel): """Response schema for the readings time-range endpoint.""" items: list[ModbusReadingResponse] class ModbusLatestResponse(BaseModel): """Response for the /latest endpoint. ``found`` is False and ``recorded_at``/``payload`` are None when the device has no readings yet. Callers should check ``found`` before using the values. """ found: bool recorded_at: datetime | None payload: dict[str, Any] | None # --------------------------------------------------------------------------- # Metrics / profile schemas # --------------------------------------------------------------------------- class MetricInfo(BaseModel): """Metadata for a single measurable quantity in a device's profile.""" key: str label: str unit: str device_class: str class ModbusMetricsResponse(BaseModel): """Response schema for GET /api/modbus/devices/{uuid}/metrics.""" profile: str metrics: list[MetricInfo] # --------------------------------------------------------------------------- # Profile list schema # --------------------------------------------------------------------------- class ProfileSummary(BaseModel): """One entry in the GET /api/modbus/profiles response.""" name: str description: str class ModbusProfilesResponse(BaseModel): """Response schema for GET /api/modbus/profiles.""" profiles: list[ProfileSummary] # --------------------------------------------------------------------------- # Test-read schema # --------------------------------------------------------------------------- class ModbusTestReadResponse(BaseModel): """Response for POST /api/modbus/devices/{uuid}/test. On success ``ok=True`` and ``payload`` contains the decoded values. On failure ``ok=False`` and ``error`` describes the problem. """ ok: bool payload: dict[str, Any] | None = None error: str | None = None # --------------------------------------------------------------------------- # Cascade-delete schema # --------------------------------------------------------------------------- class ModbusDeleteResponse(BaseModel): """Response for DELETE /api/modbus/devices/{uuid}?cascade=true. Returned only when cascade deletion succeeds (HTTP 200). Non-cascade successful deletes continue to return HTTP 204 (no body). """ deleted: bool readings_deleted: int toggles_deleted: int