diff --git a/dashboard/src/SetupMode.tsx b/dashboard/src/SetupMode.tsx index dc35718..a378e87 100644 --- a/dashboard/src/SetupMode.tsx +++ b/dashboard/src/SetupMode.tsx @@ -72,8 +72,19 @@ const SetupMode: React.FC = () => { - - + + { + if (!props.last_alive) return ''; + let iso = props.last_alive; + if (!iso.endsWith('Z')) iso += 'Z'; + const date = new Date(iso); + const pad = (n: number) => n.toString().padStart(2, '0'); + return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; + }} + /> { ...c, Id: c.uuid, Status: - c.group_id === 1 ? 'Nicht zugeordnet' : groupMap[c.group_id] || 'Nicht zugeordnet', + c.group_id === 1 + ? 'Nicht zugeordnet' + : typeof c.group_id === 'number' && groupMap[c.group_id] + ? groupMap[c.group_id] + : 'Nicht zugeordnet', Summary: c.description || `Client ${i + 1}`, })) ); @@ -166,7 +170,10 @@ const Infoscreen_groups: React.FC = () => { data.map((c, i) => ({ ...c, Id: c.uuid, - Status: groupMap[c.group_id] || 'Nicht zugeordnet', + Status: + typeof c.group_id === 'number' && groupMap[c.group_id] + ? groupMap[c.group_id] + : 'Nicht zugeordnet', Summary: c.description || `Client ${i + 1}`, })) ); @@ -200,7 +207,10 @@ const Infoscreen_groups: React.FC = () => { data.map((c, i) => ({ ...c, Id: c.uuid, - Status: groupMap[c.group_id] || 'Nicht zugeordnet', + Status: + typeof c.group_id === 'number' && groupMap[c.group_id] + ? groupMap[c.group_id] + : 'Nicht zugeordnet', Summary: c.description || `Client ${i + 1}`, })) ); @@ -276,8 +286,11 @@ const Infoscreen_groups: React.FC = () => { data.map((c, i) => ({ ...c, Id: c.uuid, - Status: groupMap[c.group_id] || 'Nicht zugeordnet', - Summary: c.location || `Client ${i + 1}`, + Status: + typeof c.group_id === 'number' && groupMap[c.group_id] + ? groupMap[c.group_id] + : 'Nicht zugeordnet', + Summary: c.description || `Client ${i + 1}`, })) ); // Nach dem Laden: Karten deselektieren diff --git a/docker-compose.yml b/docker-compose.yml index 9e398bd..30a7d22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -159,21 +159,5 @@ services: networks: - infoscreen-net - simclient: - build: - context: ./simclient - dockerfile: Dockerfile - image: infoscreen-simclient:latest - container_name: infoscreen-simclient - restart: unless-stopped - depends_on: - mqtt: - condition: service_healthy - environment: - MQTT_BROKER_URL: mqtt - MQTT_PORT: 1883 - networks: - - infoscreen-net - volumes: db-data: diff --git a/scheduler/scheduler.py b/scheduler/scheduler.py index b0350d9..ff88a02 100644 --- a/scheduler/scheduler.py +++ b/scheduler/scheduler.py @@ -33,6 +33,7 @@ def main(): # Fix für die veraltete API - explizit callback_api_version setzen client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2) client.connect("mqtt", 1883) + client.loop_start() POLL_INTERVAL = 30 # Sekunden, Empfehlung für seltene Änderungen last_payloads = {} # group_id -> payload @@ -58,16 +59,25 @@ def main(): topic = f"infoscreen/events/{gid}" payload = json.dumps(event_list) if last_payloads.get(gid) != payload: - client.publish(topic, payload, retain=True) - logging.info(f"Events für Gruppe {gid} gesendet: {payload}") + result = client.publish(topic, payload, retain=True) + if result.rc != mqtt.MQTT_ERR_SUCCESS: + logging.error( + f"Fehler beim Publish für Gruppe {gid}: {mqtt.error_string(result.rc)}") + else: + logging.info( + f"Events für Gruppe {gid} gesendet: {payload}") last_payloads[gid] = payload # Entferne Gruppen, die nicht mehr existieren (optional: retained Message löschen) for gid in list(last_payloads.keys()): if gid not in groups: topic = f"infoscreen/events/{gid}" - client.publish(topic, payload="[]", retain=True) - logging.info( - f"Events für Gruppe {gid} entfernt (leere retained Message gesendet)") + result = client.publish(topic, payload="[]", retain=True) + if result.rc != mqtt.MQTT_ERR_SUCCESS: + logging.error( + f"Fehler beim Entfernen für Gruppe {gid}: {mqtt.error_string(result.rc)}") + else: + logging.info( + f"Events für Gruppe {gid} entfernt (leere retained Message gesendet)") del last_payloads[gid] time.sleep(POLL_INTERVAL) diff --git a/simclient/Dockerfile b/simclient/Dockerfile deleted file mode 100644 index 99c954b..0000000 --- a/simclient/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM python:3.13-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -CMD ["python", "simclient.py"] diff --git a/simclient/requirements.txt b/simclient/requirements.txt deleted file mode 100644 index 02a54e4..0000000 --- a/simclient/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -paho-mqtt -dotenv -requests diff --git a/simclient/simclient.py b/simclient/simclient.py deleted file mode 100644 index 14e6bf2..0000000 --- a/simclient/simclient.py +++ /dev/null @@ -1,226 +0,0 @@ -# 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()