odoo_mqtt/iot_bridge/api/models.py
matthias.lotz ea565775b2 feat: parser_config als Wire-Format durchgehend (Phase 3 komplett)
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
2026-03-11 13:05:54 +01:00

93 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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