drop obsolete simulators
This commit is contained in:
@@ -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()
|
|
||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user