# simclient/simclient.py from logging.handlers import RotatingFileHandler import time import uuid import json import socket import hashlib import paho.mqtt.client as mqtt import os import re import platform import logging from dotenv import load_dotenv import requests # ENV laden load_dotenv("/workspace/simclient/.env") # Konfiguration aus ENV ENV = os.getenv("ENV", "development") HEARTBEAT_INTERVAL = int( os.getenv("HEARTBEAT_INTERVAL", 5 if ENV == "development" else 60)) LOG_LEVEL = os.getenv("LOG_LEVEL", "DEBUG" if ENV == "development" else "INFO") MQTT_BROKER = os.getenv("MQTT_BROKER", "mqtt") MQTT_PORT = int(os.getenv("MQTT_PORT", 1883)) DEBUG_MODE = os.getenv("DEBUG_MODE", "1" if ENV == "development" else "0") in ("1", "true", "True") # Logging-Konfiguration LOG_PATH = os.path.join(os.path.dirname(__file__), "simclient.log") os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True) log_handlers = [] log_handlers.append(RotatingFileHandler( LOG_PATH, maxBytes=2*1024*1024, backupCount=5, encoding="utf-8")) if DEBUG_MODE: log_handlers.append(logging.StreamHandler()) logging.basicConfig( level=getattr(logging, LOG_LEVEL.upper(), logging.INFO), format="%(asctime)s [%(levelname)s] %(message)s", handlers=log_handlers ) discovered = False def on_message(client, userdata, msg): global discovered logging.info(f"Empfangen: {msg.topic} {msg.payload.decode()}") # Event-Messages vom Scheduler explizit loggen if msg.topic.startswith("infoscreen/events/"): logging.info( f"Event-Message vom Scheduler empfangen: {msg.payload.decode()}") # ACK-Quittung empfangen? if msg.topic.endswith("/discovery_ack"): discovered = True logging.info("Discovery-ACK empfangen. Starte Heartbeat.") def get_mac_addresses(): macs = set() try: for root, dirs, files in os.walk('/sys/class/net/'): for iface in dirs: try: with open(f'/sys/class/net/{iface}/address') as f: mac = f.read().strip() if mac and mac != '00:00:00:00:00:00': macs.add(mac) except Exception: continue break except Exception: pass return sorted(macs) def get_board_serial(): # Raspberry Pi: /proc/cpuinfo, andere: /sys/class/dmi/id/product_serial serial = None try: with open('/proc/cpuinfo') as f: for line in f: if line.lower().startswith('serial'): serial = line.split(':')[1].strip() break except Exception: pass if not serial: try: with open('/sys/class/dmi/id/product_serial') as f: serial = f.read().strip() except Exception: pass return serial or "unknown" def get_ip(): # Versucht, die lokale IP zu ermitteln (nicht 127.0.0.1) try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except Exception: return "unknown" def get_hardware_token(): serial = get_board_serial() macs = get_mac_addresses() token_raw = serial + "_" + "_".join(macs) # Hashen für Datenschutz token_hash = hashlib.sha256(token_raw.encode()).hexdigest() return token_hash def get_model(): # Versucht, das Modell auszulesen (z.B. Raspberry Pi, PC, etc.) try: if os.path.exists('/proc/device-tree/model'): with open('/proc/device-tree/model') as f: return f.read().strip() elif os.path.exists('/sys/class/dmi/id/product_name'): with open('/sys/class/dmi/id/product_name') as f: return f.read().strip() except Exception: pass return "unknown" SOFTWARE_VERSION = "1.0.0" # Optional: Anpassen bei neuen Releases def send_discovery(client, client_id, hardware_token, ip_addr): macs = get_mac_addresses() discovery_msg = { "uuid": client_id, "hardware_token": hardware_token, "ip": ip_addr, "type": "infoscreen", "hostname": socket.gethostname(), "os_version": platform.platform(), "software_version": SOFTWARE_VERSION, "macs": macs, "model": get_model(), } client.publish("infoscreen/discovery", json.dumps(discovery_msg)) logging.info(f"Discovery-Nachricht gesendet: {discovery_msg}") def get_persistent_uuid(uuid_path="/data/client_uuid.txt"): # Prüfe, ob die Datei existiert if os.path.exists(uuid_path): with open(uuid_path, "r") as f: return f.read().strip() # Generiere neue UUID und speichere sie new_uuid = str(uuid.uuid4()) os.makedirs(os.path.dirname(uuid_path), exist_ok=True) with open(uuid_path, "w") as f: f.write(new_uuid) return new_uuid def main(): global discovered client_id = get_persistent_uuid() hardware_token = get_hardware_token() ip_addr = get_ip() client = mqtt.Client(protocol=mqtt.MQTTv311, callback_api_version=2) client.on_message = on_message client.connect(MQTT_BROKER, MQTT_PORT) # Discovery-ACK-Topic abonnieren ack_topic = f"infoscreen/{client_id}/discovery_ack" client.subscribe(ack_topic) client.subscribe(f"infoscreen/{client_id}/config") # Hilfsfunktion: Hole group_id per API def get_group_id(client_id): api_url = f"http://server:8000/api/clients/{client_id}/group" try: resp = requests.get(api_url, timeout=5) logging.debug( f"API-Request: {api_url} - Status: {resp.status_code} - Response: {resp.text}") if resp.ok: group_id = resp.json().get("group_id") logging.info( f"Ermittelte group_id für Client {client_id}: {group_id}") return group_id except Exception as e: logging.error(f"Fehler beim API-Request für group_id: {e}") return None # Discovery-Phase: Sende Discovery bis ACK empfangen while not discovered: send_discovery(client, client_id, hardware_token, ip_addr) client.loop(timeout=1.0) time.sleep(HEARTBEAT_INTERVAL) # Event-Topic abonnieren (und bei Änderung wechseln) current_group_id = None event_topic = None while True: group_id = get_group_id(client_id) logging.debug( f"Aktuelle group_id: {group_id}, vorherige group_id: {current_group_id}") if group_id != current_group_id: # Topic wechseln if event_topic: client.unsubscribe(event_topic) logging.info(f"Event-Topic abbestellt: {event_topic}") event_topic = f"infoscreen/events/{group_id}" client.subscribe(event_topic) current_group_id = group_id logging.info( f"Abonniere Event-Topic: {event_topic} für group_id: {group_id}") client.publish(f"infoscreen/{client_id}/heartbeat", "alive") logging.debug("Heartbeat gesendet.") client.loop(timeout=1.0) time.sleep(HEARTBEAT_INTERVAL) if __name__ == "__main__": main()