#!/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()