session_config wird nicht mehr über die API/YAML gesendet. parser_config ist jetzt das einzige Config-Format zwischen Odoo und Bridge. Änderungen: - api/models.py: DeviceConfig.session_config → parser_config: dict SessionConfig bleibt als internes Modell für SessionDetector DeviceConfig.to_session_config() extrahiert Werte mit Defaults - config/schema.py: DeviceConfig gleich umgestellt + to_session_config() - config/loader.py: liest parser_config aus YAML, Fallback für legacy session_config (Rückwärtskompatibilität für bestehende config-active.yaml) - core/device_manager.py: device.session_config → device.to_session_config() - core/service_manager.py: session_config Referenzen entfernt - Odoo _build_bridge_config: sendet parser_config direkt (+ heartbeat) - Odoo iot_api.py: gleich umgestellt - Tests: alle SessionConfig-Fixtures → parser_config dicts 63/63 passing
93 lines
3.9 KiB
Python
93 lines
3.9 KiB
Python
"""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
|