diff --git a/tools/sim_test_lampe.py b/tools/sim_test_lampe.py deleted file mode 100644 index 4b8d4b6..0000000 --- a/tools/sim_test_lampe.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python3 -"""MQTT Simulator for multiple test_lampe devices. - -This simulator acts as virtual light devices that: -- Subscribe to vendor/test_lampe_*/set -- Maintain local state for each device -- Publish 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")) - -# Devices to simulate -DEVICES = ["test_lampe_1", "test_lampe_2", "test_lampe_3"] - -# Device states (one per device) -device_states = { - "test_lampe_1": { - "power": "off", - "brightness": 50 - }, - "test_lampe_2": { - "power": "off", - "brightness": 50 - }, - "test_lampe_3": { - "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 topics for all devices - for device_id in DEVICES: - set_topic = f"vendor/{device_id}/set" - client.subscribe(set_topic, qos=1) - logger.info(f"Subscribed to {set_topic}") - - # Publish initial states (retained) - for device_id in DEVICES: - publish_state(client, device_id) - logger.info(f"Simulator started for {device_id}, initial state: {device_states[device_id]}") - 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 - """ - # Extract device_id from topic (vendor/test_lampe_X/set) - topic_parts = msg.topic.split('/') - if len(topic_parts) != 3 or topic_parts[0] != "vendor" or topic_parts[2] != "set": - logger.warning(f"Unexpected topic format: {msg.topic}") - return - - device_id = topic_parts[1] - - if device_id not in device_states: - logger.warning(f"Unknown device: {device_id}") - return - - try: - payload = json.loads(msg.payload.decode()) - logger.info(f"[{device_id}] Received SET command: {payload}") - - # Update device state - updated = False - device_state = device_states[device_id] - - 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"[{device_id}] 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"[{device_id}] Brightness changed: {old_brightness} -> {device_state['brightness']}") - - # Publish updated state if changed - if updated: - publish_state(client, device_id) - logger.info(f"[{device_id}] Published new state: {device_state}") - - except json.JSONDecodeError as e: - logger.error(f"[{device_id}] Invalid JSON in message: {e}") - except Exception as e: - logger.error(f"[{device_id}] Error processing message: {e}") - - -def publish_state(client, device_id): - """Publish current device state to STATE topic. - - Args: - client: MQTT client instance - device_id: Device identifier - """ - device_state = device_states[device_id] - state_topic = f"vendor/{device_id}/state" - 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"[{device_id}] Published state to {state_topic}: {state_json}") - else: - logger.error(f"[{device_id}] 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 for all devices before disconnecting - for device_id in DEVICES: - offline_state = device_states[device_id].copy() - offline_state["power"] = "off" - state_topic = f"vendor/{device_id}/state" - client_global.publish(state_topic, json.dumps(offline_state), qos=1, retain=True) - logger.info(f"[{device_id}] 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="simulator-test-lampes", - 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() diff --git a/tools/sim_thermo.py b/tools/sim_thermo.py deleted file mode 100755 index 79a1874..0000000 --- a/tools/sim_thermo.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python3 -""" -MQTT Simulator für test_thermo_1 Thermostat. - -Funktionalität: -- Subscribt auf vendor/test_thermo_1/set -- Hält internen Zustand (mode, target, current, battery, window_open) -- Reagiert auf SET-Commands (mode/target) -- Simuliert Temperatur-Drift zu target (alle 5s, max ±0.2°C) -- Publiziert vendor/test_thermo_1/state (retained, QoS 1) - -Usage: - poetry run python tools/sim_thermo.py - -Environment Variables: - MQTT_BROKER: MQTT broker hostname (default: 172.16.2.16) - MQTT_PORT: MQTT broker port (default: 1883) - -Test Commands: - # Start simulator - poetry run python tools/sim_thermo.py & - - # Test SET command - curl -X POST http://localhost:8001/devices/test_thermo_1/set \ - -H "Content-Type: application/json" \ - -d '{"type":"thermostat","payload":{"mode":"heat","target":22.5}}' - - # Monitor state - mosquitto_sub -h 172.16.2.16 -t 'vendor/test_thermo_1/state' -v -""" - -import asyncio -import json -import os -import sys -from datetime import datetime -from aiomqtt import Client, MqttError - - -# Configuration -BROKER_HOST = os.getenv("MQTT_BROKER", "172.16.2.16") -BROKER_PORT = int(os.getenv("MQTT_PORT", "1883")) -DEVICE_ID = "test_thermo_1" -SET_TOPIC = f"vendor/{DEVICE_ID}/set" -STATE_TOPIC = f"vendor/{DEVICE_ID}/state" -DRIFT_INTERVAL = 5 # seconds - - -class ThermostatSimulator: - """Simulates a thermostat device with temperature regulation.""" - - def __init__(self): - self.state = { - "mode": "auto", - "target": 21.0, - "current": 20.5, - "battery": 90, - "window_open": False - } - self.client = None - self.running = True - - def log(self, msg: str): - """Log with timestamp.""" - timestamp = datetime.now().strftime("%H:%M:%S") - print(f"[{timestamp}] {msg}", flush=True) - - async def publish_state(self): - """Publish current state to MQTT (retained, QoS 1).""" - if not self.client: - return - - payload = json.dumps(self.state) - await self.client.publish( - STATE_TOPIC, - payload=payload, - qos=1, - retain=True - ) - self.log(f"📤 Published state: {payload}") - - def apply_temperature_drift(self): - """ - Simulate temperature drift towards target. - Max change: ±0.2°C per interval. - """ - if self.state["mode"] == "off": - # In OFF mode, drift slowly towards ambient (assume 18°C) - ambient = 18.0 - diff = ambient - self.state["current"] - else: - # In HEAT/AUTO mode, drift towards target - diff = self.state["target"] - self.state["current"] - - # Apply max ±0.2°C drift - if abs(diff) < 0.1: - # Close enough, small adjustment - self.state["current"] = round(self.state["current"] + diff, 1) - elif diff > 0: - self.state["current"] = round(self.state["current"] + 0.2, 1) - else: - self.state["current"] = round(self.state["current"] - 0.2, 1) - - self.log(f"🌡️ Temperature drift: current={self.state['current']}°C (target={self.state['target']}°C)") - - async def handle_set_command(self, payload: dict): - """ - Handle SET command from MQTT. - Payload can contain: mode, target - """ - self.log(f"📥 Received SET: {payload}") - - changed = False - - if "mode" in payload: - new_mode = payload["mode"] - if new_mode in ["off", "heat", "auto"]: - self.state["mode"] = new_mode - changed = True - self.log(f" Mode changed to: {new_mode}") - else: - self.log(f" ⚠️ Invalid mode: {new_mode}") - - if "target" in payload: - try: - new_target = float(payload["target"]) - if 5.0 <= new_target <= 30.0: - self.state["target"] = new_target - changed = True - self.log(f" Target changed to: {new_target}°C") - else: - self.log(f" ⚠️ Target out of range: {new_target}") - except (ValueError, TypeError): - self.log(f" ⚠️ Invalid target value: {payload['target']}") - - if changed: - await self.publish_state() - - async def drift_loop(self): - """Background loop for temperature drift simulation.""" - while self.running: - await asyncio.sleep(DRIFT_INTERVAL) - self.apply_temperature_drift() - await self.publish_state() - - async def mqtt_loop(self): - """Main MQTT connection and message handling loop.""" - try: - async with Client( - hostname=BROKER_HOST, - port=BROKER_PORT, - identifier=f"sim_{DEVICE_ID}" - ) as client: - self.client = client - self.log(f"✅ Connected to MQTT broker {BROKER_HOST}:{BROKER_PORT}") - - # Publish initial state - await self.publish_state() - self.log(f"📡 Thermo sim started for {DEVICE_ID}") - - # Subscribe to SET topic - await client.subscribe(SET_TOPIC, qos=1) - self.log(f"👂 Subscribed to {SET_TOPIC}") - - # Start drift loop in background - drift_task = asyncio.create_task(self.drift_loop()) - - # Listen for messages - async for message in client.messages: - try: - payload = json.loads(message.payload.decode()) - await self.handle_set_command(payload) - except json.JSONDecodeError as e: - self.log(f"❌ Invalid JSON: {e}") - except Exception as e: - self.log(f"❌ Error handling message: {e}") - - # Cancel drift loop on disconnect - drift_task.cancel() - - except MqttError as e: - self.log(f"❌ MQTT Error: {e}") - except KeyboardInterrupt: - self.log("⚠️ Interrupted by user") - finally: - self.running = False - self.log("👋 Simulator stopped") - - async def run(self): - """Run the simulator.""" - await self.mqtt_loop() - - -async def main(): - """Entry point.""" - simulator = ThermostatSimulator() - await simulator.run() - - -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - print("\n👋 Simulator terminated") - sys.exit(0)