odoo_mqtt/iot_bridge/tests/conftest.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

346 lines
9.6 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.

"""
Shared pytest fixtures for IoT Bridge tests.
This module provides reusable test fixtures for:
- Configuration objects
- MQTT client mocks
- Odoo client mocks
- Device fixtures
- Session detector instances
- Event queue instances
"""
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, Mock
import pytest
import yaml
from config.schema import (
BridgeConfig,
DeviceConfig,
DeviceStatusConfig,
EventQueueConfig,
LoggingConfig,
MQTTConfig,
OdooConfig,
SessionConfig,
)
# ============================================================================
# Configuration Fixtures
# ============================================================================
@pytest.fixture
def temp_config_file():
"""Create a temporary YAML config file for testing."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
config_data = {
"mqtt": {
"broker": "localhost",
"port": 1883,
"client_id": "test_bridge",
"username": "",
"password": "",
"use_tls": False,
},
"odoo": {
"base_url": "http://localhost:8069",
"database": "test_db",
"username": "test@example.com",
"password": "test_password",
},
"logging": {"level": "DEBUG", "format": "json"},
"session": {
"heartbeat_interval": 300,
"grace_period": 60,
"min_session_duration": 10,
},
"devices": [
{
"device_id": "test_device_1",
"topic": "test/device1/#",
"parser": "shelly",
},
{
"device_id": "test_device_2",
"topic": "test/device2/#",
"parser": "shelly",
},
],
}
yaml.dump(config_data, f)
temp_path = Path(f.name)
yield temp_path
# Cleanup
temp_path.unlink(missing_ok=True)
@pytest.fixture
def mqtt_config():
"""Provide a sample MQTT configuration."""
return MQTTConfig(
broker="localhost",
port=1883,
client_id="test_bridge",
username="",
password="",
use_tls=False,
)
@pytest.fixture
def odoo_config():
"""Provide a sample Odoo configuration."""
return OdooConfig(
base_url="http://localhost:8069",
database="test_db",
username="test@example.com",
api_key="test_password",
)
@pytest.fixture
def logging_config():
"""Provide a sample logging configuration."""
return LoggingConfig(level="DEBUG", format="json", log_file=None)
@pytest.fixture
def session_config():
"""Provide a sample session configuration (legacy derived from parser_config)."""
return SessionConfig(
strategy="power_threshold",
standby_threshold_w=5.0,
working_threshold_w=50.0,
start_debounce_s=3.0,
stop_debounce_s=15.0,
message_timeout_s=20.0,
heartbeat_interval_s=300.0,
)
@pytest.fixture
def device_config():
"""Provide a sample device configuration."""
return DeviceConfig(
device_id="test_device",
mqtt_topic="test/device/#",
parser_type="shelly_pm",
machine_name="Test Device",
parser_config={
"standby_threshold_w": 5.0,
"working_threshold_w": 50.0,
"start_debounce_s": 3.0,
"stop_debounce_s": 15.0,
"message_timeout_s": 20.0,
"heartbeat_interval_s": 300.0,
},
)
@pytest.fixture
def device_configs():
"""Provide a list of device configurations."""
_cfg = {
"standby_threshold_w": 5.0,
"working_threshold_w": 50.0,
"start_debounce_s": 3.0,
"stop_debounce_s": 15.0,
"message_timeout_s": 20.0,
"heartbeat_interval_s": 300.0,
}
return [
DeviceConfig(
device_id="test_device_1",
mqtt_topic="test/device1/#",
parser_type="shelly_pm",
machine_name="Test Device 1",
parser_config=_cfg,
),
DeviceConfig(
device_id="test_device_2",
mqtt_topic="test/device2/#",
parser_type="shelly_pm",
machine_name="Test Device 2",
parser_config=_cfg,
),
]
@pytest.fixture
def bridge_config(mqtt_config, odoo_config, logging_config, device_configs):
"""Provide a complete bridge configuration."""
return BridgeConfig(
mqtt=mqtt_config,
odoo=odoo_config,
logging=logging_config,
event_queue=EventQueueConfig(),
device_status=DeviceStatusConfig(),
devices=device_configs,
)
# ============================================================================
# MQTT Client Fixtures
# ============================================================================
@pytest.fixture
def mock_mqtt_client():
"""Provide a mock MQTT client."""
client = MagicMock()
client.connect = Mock(return_value=True)
client.disconnect = Mock()
client.subscribe = Mock()
client.unsubscribe = Mock()
client.publish = Mock()
client.loop_start = Mock()
client.loop_stop = Mock()
client.is_connected = Mock(return_value=True)
return client
@pytest.fixture
def mock_paho_mqtt_client(monkeypatch):
"""Mock paho.mqtt.client.Client for tests that instantiate it."""
mock_client = MagicMock()
mock_client.connect = Mock(return_value=0) # MQTT success code
mock_client.disconnect = Mock(return_value=0)
mock_client.subscribe = Mock(return_value=(0, 1)) # (result, mid)
mock_client.unsubscribe = Mock(return_value=(0, 1))
mock_client.publish = Mock(return_value=(0, 1))
mock_client.loop_start = Mock(return_value=0)
mock_client.loop_stop = Mock(return_value=0)
def mock_mqtt_client_init(*args, **kwargs):
return mock_client
monkeypatch.setattr("paho.mqtt.client.Client", mock_mqtt_client_init)
return mock_client
# ============================================================================
# Odoo Client Fixtures
# ============================================================================
@pytest.fixture
def mock_odoo_client():
"""Provide a mock Odoo client."""
client = MagicMock()
client.send_event = Mock(return_value=True)
client.authenticate = Mock(return_value=True)
client.base_url = "http://localhost:8069"
client.database = "test_db"
return client
@pytest.fixture
def mock_requests(monkeypatch):
"""Mock requests library for HTTP calls."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json = Mock(return_value={"result": "success"})
mock_response.raise_for_status = Mock()
mock_post = Mock(return_value=mock_response)
mock_get = Mock(return_value=mock_response)
monkeypatch.setattr("requests.post", mock_post)
monkeypatch.setattr("requests.get", mock_get)
return {"post": mock_post, "get": mock_get, "response": mock_response}
# ============================================================================
# Device & Parser Fixtures
# ============================================================================
@pytest.fixture
def shelly_pm_message():
"""Provide a sample Shelly PM MQTT message."""
return {
"topic": "shellypmminig3-test123/status/pm1:0",
"payload": {
"id": 0,
"voltage": 230.5,
"current": 2.1,
"apower": 484.0,
"freq": 50.0,
"aenergy": {"total": 12345.678, "by_minute": [120.5, 115.3, 118.7]},
},
}
@pytest.fixture
def mock_parser():
"""Provide a mock message parser."""
parser = MagicMock()
parser.parse_message = Mock(
return_value={
"device_id": "test_device",
"event_type": "power",
"power": 484.0,
"voltage": 230.5,
"current": 2.1,
"energy": 12345.678,
"timestamp": "2026-02-18T20:00:00Z",
}
)
return parser
# ============================================================================
# Session Detector Fixtures
# ============================================================================
@pytest.fixture
def session_detector_config():
"""Provide configuration for session detector."""
return {
"device_id": "test_device",
"heartbeat_interval": 300,
"grace_period": 60,
"min_session_duration": 10,
"power_threshold": 50.0,
}
# ============================================================================
# Event Queue Fixtures
# ============================================================================
@pytest.fixture
def sample_event():
"""Provide a sample IoT event."""
return {
"device_id": "test_device",
"event_type": "session_start",
"timestamp": "2026-02-18T20:00:00Z",
"power": 484.0,
"energy": 12345.678,
}
# ============================================================================
# Pytest Configuration
# ============================================================================
def pytest_configure(config):
"""Configure pytest with custom markers."""
config.addinivalue_line("markers", "unit: Unit tests (fast, no external dependencies)")
config.addinivalue_line(
"markers", "integration: Integration tests (may require MQTT broker, Odoo)"
)
config.addinivalue_line("markers", "e2e: End-to-end tests (full system test)")
config.addinivalue_line("markers", "slow: Slow running tests")