zwei Lampen und Test-Werkzeug

This commit is contained in:
2025-10-31 15:17:28 +01:00
parent ea17d048ad
commit c3ec6e3fc4
7 changed files with 949 additions and 43 deletions

186
tools/sim_test_lampe.py Normal file
View File

@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""MQTT Simulator for test_lampe device.
This simulator acts as a virtual light device that:
- Subscribes to vendor/test_lampe/set
- Maintains local state
- Publishes state changes to vendor/test_lampe/state (retained)
"""
import json
import logging
import os
import signal
import sys
import time
import paho.mqtt.client as mqtt
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# Configuration
BROKER_HOST = os.environ.get("MQTT_HOST", "172.16.2.16")
BROKER_PORT = int(os.environ.get("MQTT_PORT", "1883"))
DEVICE_ID = "test_lampe_1"
SET_TOPIC = f"vendor/{DEVICE_ID}/set"
STATE_TOPIC = f"vendor/{DEVICE_ID}/state"
# Device state
device_state = {
"power": "off",
"brightness": 50
}
# Global client for signal handler
client_global = None
def on_connect(client, userdata, flags, rc, properties=None):
"""Callback when connected to MQTT broker.
Args:
client: MQTT client instance
userdata: User data
flags: Connection flags
rc: Connection result code
properties: Connection properties (MQTT v5)
"""
if rc == 0:
logger.info(f"Connected to MQTT broker {BROKER_HOST}:{BROKER_PORT}")
# Subscribe to SET topic
client.subscribe(SET_TOPIC, qos=1)
logger.info(f"Subscribed to {SET_TOPIC}")
# Publish initial state (retained)
publish_state(client)
logger.info(f"Simulator started, initial state published: {device_state}")
else:
logger.error(f"Connection failed with code {rc}")
def on_message(client, userdata, msg):
"""Callback when message received on subscribed topic.
Args:
client: MQTT client instance
userdata: User data
msg: MQTT message
"""
global device_state
try:
payload = json.loads(msg.payload.decode())
logger.info(f"Received SET command: {payload}")
# Update device state
updated = False
if "power" in payload:
old_power = device_state["power"]
device_state["power"] = payload["power"]
if old_power != device_state["power"]:
updated = True
logger.info(f"Power changed: {old_power} -> {device_state['power']}")
if "brightness" in payload:
old_brightness = device_state["brightness"]
device_state["brightness"] = int(payload["brightness"])
if old_brightness != device_state["brightness"]:
updated = True
logger.info(f"Brightness changed: {old_brightness} -> {device_state['brightness']}")
# Publish updated state if changed
if updated:
publish_state(client)
logger.info(f"Published new state: {device_state}")
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON in message: {e}")
except Exception as e:
logger.error(f"Error processing message: {e}")
def publish_state(client):
"""Publish current device state to STATE topic.
Args:
client: MQTT client instance
"""
state_json = json.dumps(device_state)
result = client.publish(STATE_TOPIC, state_json, qos=1, retain=True)
if result.rc == mqtt.MQTT_ERR_SUCCESS:
logger.debug(f"Published state to {STATE_TOPIC}: {state_json}")
else:
logger.error(f"Failed to publish state: {result.rc}")
def signal_handler(sig, frame):
"""Handle shutdown signals gracefully.
Args:
sig: Signal number
frame: Current stack frame
"""
logger.info(f"Received signal {sig}, shutting down...")
if client_global:
# Publish offline state before disconnecting
offline_state = device_state.copy()
offline_state["power"] = "off"
client_global.publish(STATE_TOPIC, json.dumps(offline_state), qos=1, retain=True)
logger.info("Published offline state")
client_global.disconnect()
client_global.loop_stop()
sys.exit(0)
def main():
"""Main entry point for the simulator."""
global client_global
# Setup signal handlers
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Create MQTT client
client = mqtt.Client(
client_id=f"simulator-{DEVICE_ID}",
protocol=mqtt.MQTTv5,
callback_api_version=mqtt.CallbackAPIVersion.VERSION2
)
client_global = client
# Set callbacks
client.on_connect = on_connect
client.on_message = on_message
# Connect to broker
logger.info(f"Connecting to MQTT broker {BROKER_HOST}:{BROKER_PORT}...")
try:
client.connect(BROKER_HOST, BROKER_PORT, keepalive=60)
# Start network loop
client.loop_forever()
except KeyboardInterrupt:
logger.info("Interrupted by user")
except Exception as e:
logger.error(f"Error: {e}")
finally:
if client.is_connected():
client.disconnect()
client.loop_stop()
if __name__ == "__main__":
main()

253
tools/test_config_loader.py Normal file
View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""Test configuration loader with different YAML formats."""
import sys
import tempfile
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from apps.abstraction.main import load_config, validate_devices
def test_device_id_format():
"""Test YAML with 'device_id' key."""
yaml_content = """version: 1
mqtt:
broker: "172.16.2.16"
port: 1883
client_id: "test"
redis:
url: "redis://localhost:6379/8"
channel: "ui:updates"
devices:
- device_id: test_lampe
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
topics:
set: "vendor/test_lampe/set"
state: "vendor/test_lampe/state"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = Path(f.name)
try:
config = load_config(temp_path)
devices = config.get("devices", [])
validate_devices(devices)
print("✓ Test 1 passed: YAML with 'device_id' loaded successfully")
print(f" Device ID: {devices[0]['device_id']}")
return True
except Exception as e:
print(f"✗ Test 1 failed: {e}")
return False
finally:
temp_path.unlink()
def test_id_format():
"""Test YAML with 'id' key (legacy format)."""
yaml_content = """version: 1
mqtt:
broker: "172.16.2.16"
port: 1883
client_id: "test"
redis:
url: "redis://localhost:6379/8"
channel: "ui:updates"
devices:
- id: test_lampe
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
topics:
set: "vendor/test_lampe/set"
state: "vendor/test_lampe/state"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = Path(f.name)
try:
config = load_config(temp_path)
devices = config.get("devices", [])
validate_devices(devices)
print("✓ Test 2 passed: YAML with 'id' loaded successfully")
print(f" Device ID: {devices[0]['device_id']}")
return True
except Exception as e:
print(f"✗ Test 2 failed: {e}")
return False
finally:
temp_path.unlink()
def test_missing_id():
"""Test YAML without 'id' or 'device_id' key."""
yaml_content = """version: 1
mqtt:
broker: "172.16.2.16"
port: 1883
devices:
- type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
topics:
set: "vendor/test_lampe/set"
state: "vendor/test_lampe/state"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = Path(f.name)
try:
config = load_config(temp_path)
devices = config.get("devices", [])
validate_devices(devices)
print("✗ Test 3 failed: Should have raised ValueError for missing device_id")
return False
except ValueError as e:
if "requires 'id' or 'device_id'" in str(e):
print(f"✓ Test 3 passed: Correct error for missing device_id")
print(f" Error: {e}")
return True
else:
print(f"✗ Test 3 failed: Wrong error message: {e}")
return False
except Exception as e:
print(f"✗ Test 3 failed: Unexpected error: {e}")
return False
finally:
temp_path.unlink()
def test_missing_topics_set():
"""Test YAML without 'topics.set'."""
yaml_content = """version: 1
mqtt:
broker: "172.16.2.16"
port: 1883
devices:
- device_id: test_lampe
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
topics:
state: "vendor/test_lampe/state"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = Path(f.name)
try:
config = load_config(temp_path)
devices = config.get("devices", [])
validate_devices(devices)
print("✗ Test 4 failed: Should have raised ValueError for missing topics.set")
return False
except ValueError as e:
if "missing 'topics.set'" in str(e):
print(f"✓ Test 4 passed: Correct error for missing topics.set")
print(f" Error: {e}")
return True
else:
print(f"✗ Test 4 failed: Wrong error message: {e}")
return False
except Exception as e:
print(f"✗ Test 4 failed: Unexpected error: {e}")
return False
finally:
temp_path.unlink()
def test_missing_topics_state():
"""Test YAML without 'topics.state'."""
yaml_content = """version: 1
mqtt:
broker: "172.16.2.16"
port: 1883
devices:
- device_id: test_lampe
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
topics:
set: "vendor/test_lampe/set"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
f.write(yaml_content)
temp_path = Path(f.name)
try:
config = load_config(temp_path)
devices = config.get("devices", [])
validate_devices(devices)
print("✗ Test 5 failed: Should have raised ValueError for missing topics.state")
return False
except ValueError as e:
if "missing 'topics.state'" in str(e):
print(f"✓ Test 5 passed: Correct error for missing topics.state")
print(f" Error: {e}")
return True
else:
print(f"✗ Test 5 failed: Wrong error message: {e}")
return False
except Exception as e:
print(f"✗ Test 5 failed: Unexpected error: {e}")
return False
finally:
temp_path.unlink()
def main():
"""Run all tests."""
print("Testing Configuration Loader")
print("=" * 60)
tests = [
test_device_id_format,
test_id_format,
test_missing_id,
test_missing_topics_set,
test_missing_topics_state,
]
results = []
for test in tests:
results.append(test())
print()
print("=" * 60)
passed = sum(results)
total = len(results)
print(f"Results: {passed}/{total} tests passed")
return 0 if all(results) else 1
if __name__ == "__main__":
sys.exit(main())