zwei Lampen und Test-Werkzeug
This commit is contained in:
186
tools/sim_test_lampe.py
Normal file
186
tools/sim_test_lampe.py
Normal 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
253
tools/test_config_loader.py
Normal 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())
|
||||
Reference in New Issue
Block a user