odoo_mqtt/iot_bridge/config/loader.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

156 lines
4.9 KiB
Python

"""Configuration loading functions."""
from pathlib import Path
import yaml
from config.schema import (
BridgeConfig,
DeviceConfig,
DeviceStatusConfig,
EnvOverrideSettings,
EventQueueConfig,
LoggingConfig,
MQTTConfig,
OdooConfig,
SessionConfig,
)
from exceptions import ConfigurationError
def _apply_environment_overrides(mqtt_data: dict, odoo_data: dict) -> tuple[dict, dict]:
"""Apply environment variable overrides for MQTT and Odoo settings."""
env = EnvOverrideSettings()
mqtt_overrides = {
"broker": env.mqtt_broker,
"port": env.mqtt_port,
"username": env.mqtt_username,
"password": env.mqtt_password,
"client_id": env.mqtt_client_id,
"keepalive": env.mqtt_keepalive,
"use_tls": env.mqtt_use_tls,
}
odoo_overrides = {
"base_url": env.odoo_base_url,
"database": env.odoo_database,
"username": env.odoo_username,
"api_key": env.odoo_api_key,
}
merged_mqtt = {**mqtt_data, **{key: value for key, value in mqtt_overrides.items() if value is not None}}
merged_odoo = {**odoo_data, **{key: value for key, value in odoo_overrides.items() if value is not None}}
return merged_mqtt, merged_odoo
def load_config(config_path: str = "config.yaml") -> BridgeConfig:
"""Load configuration from YAML file.
Resolution order:
1. YAML values from ``config_path``
2. Environment variable overrides (via ``EnvOverrideSettings``)
3. Pydantic default values from schema models
Args:
config_path: Path to YAML configuration file.
Returns:
Fully validated bridge configuration object.
Raises:
ConfigurationError: If the config file does not exist.
"""
path = Path(config_path)
if not path.exists():
raise ConfigurationError(
f"Config file not found: {config_path}",
path=str(path.absolute()),
)
with open(path) as f:
data = yaml.safe_load(f)
mqtt_data, odoo_data = _apply_environment_overrides(data["mqtt"], data.get("odoo", {}))
mqtt_config = MQTTConfig(**mqtt_data)
odoo_config = OdooConfig(**odoo_data)
logging_config = LoggingConfig(**data.get("logging", {}))
event_queue_config = EventQueueConfig(**data.get("event_queue", {}))
device_status_config = DeviceStatusConfig(**data.get("device_status", {}))
devices = []
for dev_data in data.get("devices", []):
# Support both new parser_config and legacy session_config format
if "parser_config" in dev_data:
parser_cfg = dev_data["parser_config"] or {}
elif "session_config" in dev_data:
# Legacy: flatten session_config into parser_config
parser_cfg = {k: v for k, v in dev_data["session_config"].items()
if k != "strategy"}
else:
parser_cfg = {}
device = DeviceConfig(
device_id=dev_data["device_id"],
mqtt_topic=dev_data["mqtt_topic"],
parser_type=dev_data.get("parser_type", "shelly_pm"),
machine_name=dev_data["machine_name"],
parser_config=parser_cfg,
)
devices.append(device)
return BridgeConfig(
mqtt=mqtt_config,
odoo=odoo_config,
logging=logging_config,
event_queue=event_queue_config,
device_status=device_status_config,
devices=devices,
)
def load_devices_from_config(config_path: str) -> list[DeviceConfig]:
"""Load only devices from a config file (for config-active.yaml).
Args:
config_path: Path to persisted active configuration.
Returns:
List of validated ``DeviceConfig`` entries.
Raises:
ConfigurationError: If the config file does not exist.
"""
path = Path(config_path)
if not path.exists():
raise ConfigurationError(
f"Config file not found: {config_path}",
path=str(path.absolute()),
)
with open(path) as f:
data = yaml.safe_load(f)
devices = []
for dev_data in data.get("devices", []):
# Support both new parser_config and legacy session_config format
# (config-active.yaml may still contain session_config from older pushes)
if "parser_config" in dev_data:
parser_cfg = dev_data["parser_config"] or {}
elif "session_config" in dev_data:
# Legacy: flatten session_config into parser_config
parser_cfg = {k: v for k, v in dev_data["session_config"].items()
if k != "strategy"}
else:
parser_cfg = {}
device = DeviceConfig(
device_id=dev_data["device_id"],
mqtt_topic=dev_data["mqtt_topic"],
parser_type=dev_data.get("parser_type", "shelly_pm"),
machine_name=dev_data["machine_name"],
parser_config=parser_cfg,
)
devices.append(device)
return devices