"""Pydantic models for API validation.""" from datetime import datetime from pydantic import BaseModel, Field, field_validator class MqttConfig(BaseModel): """MQTT Broker configuration.""" broker: str = Field("localhost", description="MQTT broker hostname") port: int = Field(1883, description="MQTT broker port") username: str = Field("", description="MQTT username (optional)") password: str = Field("", description="MQTT password (optional)") client_id: str = Field("iot_bridge", description="MQTT client ID") keepalive: int = Field(60, description="MQTT keepalive interval (s)") use_tls: bool = Field(False, description="Use TLS/SSL for MQTT connection") class SessionConfig(BaseModel): """Internal session detection parameters – derived from parser_config. Not sent over the wire. Used by SessionDetector internally. """ standby_threshold_w: float = Field(20.0, gt=0, description="Power threshold for session start (W)") working_threshold_w: float = Field(100.0, gt=0, description="Power threshold for working state (W)") start_debounce_s: float = Field(3.0, gt=0, description="Debounce time for session start (s)") stop_debounce_s: float = Field(15.0, gt=0, description="Debounce time for session stop (s)") message_timeout_s: float = Field(20.0, gt=0, description="Max time without messages before timeout (s)") heartbeat_interval_s: float = Field(300.0, gt=0, description="Interval for heartbeat events (s)") class DeviceConfig(BaseModel): """Configuration for a single IoT device.""" device_id: str = Field(..., min_length=1, description="Unique device identifier") machine_name: str = Field(..., min_length=1, description="Human-readable machine name") mqtt_topic: str = Field(..., min_length=1, description="MQTT topic to subscribe to") parser_type: str = Field("shelly_pm", description="Parser type (e.g., 'shelly_pm')") parser_config: dict = Field(default_factory=dict, description="Parser-specific configuration (from Odoo)") device_status_timeout_s: int = Field( 120, ge=10, le=600, description="Device offline timeout in seconds" ) def to_session_config(self) -> SessionConfig: """Extract SessionDetector parameters from parser_config with defaults.""" cfg = self.parser_config or {} return SessionConfig( standby_threshold_w=float(cfg.get("standby_threshold_w", 20.0)), working_threshold_w=float(cfg.get("working_threshold_w", 100.0)), start_debounce_s=float(cfg.get("start_debounce_s", 3.0)), stop_debounce_s=float(cfg.get("stop_debounce_s", 15.0)), message_timeout_s=float(cfg.get("message_timeout_s", 20.0)), heartbeat_interval_s=float(cfg.get("heartbeat_interval_s", 300.0)), ) class BridgeConfig(BaseModel): """Complete bridge configuration from Odoo.""" mqtt: MqttConfig | None = Field(None, description="MQTT broker configuration (optional)") devices: list[DeviceConfig] = Field( default_factory=list, description="List of devices to monitor" ) timestamp: str | None = Field( default_factory=lambda: datetime.utcnow().isoformat(), description="Config timestamp" ) version: str | None = Field("1.0", description="Config format version") @field_validator("devices") @classmethod def validate_unique_constraints(cls, v): """Validate uniqueness of device IDs and MQTT topics. Args: v: List of device configuration models. Returns: Unchanged validated device list. """ # Check unique device IDs device_ids = [d.device_id for d in v] if len(device_ids) != len(set(device_ids)): raise ValueError("device_id must be unique") # Check unique MQTT topics topics = [d.mqtt_topic for d in v] if len(topics) != len(set(topics)): raise ValueError("mqtt_topic must be unique") return v