remove simclient and update setup mode
This commit is contained in:
@@ -72,8 +72,19 @@ const SetupMode: React.FC = () => {
|
|||||||
<ColumnsDirective>
|
<ColumnsDirective>
|
||||||
<ColumnDirective field="uuid" headerText="UUID" width="180" />
|
<ColumnDirective field="uuid" headerText="UUID" width="180" />
|
||||||
<ColumnDirective field="hostname" headerText="Hostname" width="140" />
|
<ColumnDirective field="hostname" headerText="Hostname" width="140" />
|
||||||
<ColumnDirective field="ip_address" headerText="IP" width="120" />
|
<ColumnDirective field="ip" headerText="IP" width="120" />
|
||||||
<ColumnDirective field="last_alive" headerText="Letzter Kontakt" width="160" />
|
<ColumnDirective
|
||||||
|
headerText="Letzter Kontakt"
|
||||||
|
width="160"
|
||||||
|
template={(props: Client) => {
|
||||||
|
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())}`;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<ColumnDirective
|
<ColumnDirective
|
||||||
headerText="Beschreibung"
|
headerText="Beschreibung"
|
||||||
width="220"
|
width="220"
|
||||||
|
|||||||
@@ -107,7 +107,11 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
...c,
|
...c,
|
||||||
Id: c.uuid,
|
Id: c.uuid,
|
||||||
Status:
|
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}`,
|
Summary: c.description || `Client ${i + 1}`,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -166,7 +170,10 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
data.map((c, i) => ({
|
data.map((c, i) => ({
|
||||||
...c,
|
...c,
|
||||||
Id: c.uuid,
|
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}`,
|
Summary: c.description || `Client ${i + 1}`,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -200,7 +207,10 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
data.map((c, i) => ({
|
data.map((c, i) => ({
|
||||||
...c,
|
...c,
|
||||||
Id: c.uuid,
|
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}`,
|
Summary: c.description || `Client ${i + 1}`,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -276,8 +286,11 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
data.map((c, i) => ({
|
data.map((c, i) => ({
|
||||||
...c,
|
...c,
|
||||||
Id: c.uuid,
|
Id: c.uuid,
|
||||||
Status: groupMap[c.group_id] || 'Nicht zugeordnet',
|
Status:
|
||||||
Summary: c.location || `Client ${i + 1}`,
|
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
|
// Nach dem Laden: Karten deselektieren
|
||||||
|
|||||||
@@ -159,21 +159,5 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- infoscreen-net
|
- 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:
|
volumes:
|
||||||
db-data:
|
db-data:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ def main():
|
|||||||
# Fix für die veraltete API - explizit callback_api_version setzen
|
# Fix für die veraltete API - explizit callback_api_version setzen
|
||||||
client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
|
client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
|
||||||
client.connect("mqtt", 1883)
|
client.connect("mqtt", 1883)
|
||||||
|
client.loop_start()
|
||||||
|
|
||||||
POLL_INTERVAL = 30 # Sekunden, Empfehlung für seltene Änderungen
|
POLL_INTERVAL = 30 # Sekunden, Empfehlung für seltene Änderungen
|
||||||
last_payloads = {} # group_id -> payload
|
last_payloads = {} # group_id -> payload
|
||||||
@@ -58,16 +59,25 @@ def main():
|
|||||||
topic = f"infoscreen/events/{gid}"
|
topic = f"infoscreen/events/{gid}"
|
||||||
payload = json.dumps(event_list)
|
payload = json.dumps(event_list)
|
||||||
if last_payloads.get(gid) != payload:
|
if last_payloads.get(gid) != payload:
|
||||||
client.publish(topic, payload, retain=True)
|
result = client.publish(topic, payload, retain=True)
|
||||||
logging.info(f"Events für Gruppe {gid} gesendet: {payload}")
|
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
|
last_payloads[gid] = payload
|
||||||
# Entferne Gruppen, die nicht mehr existieren (optional: retained Message löschen)
|
# Entferne Gruppen, die nicht mehr existieren (optional: retained Message löschen)
|
||||||
for gid in list(last_payloads.keys()):
|
for gid in list(last_payloads.keys()):
|
||||||
if gid not in groups:
|
if gid not in groups:
|
||||||
topic = f"infoscreen/events/{gid}"
|
topic = f"infoscreen/events/{gid}"
|
||||||
client.publish(topic, payload="[]", retain=True)
|
result = client.publish(topic, payload="[]", retain=True)
|
||||||
logging.info(
|
if result.rc != mqtt.MQTT_ERR_SUCCESS:
|
||||||
f"Events für Gruppe {gid} entfernt (leere retained Message gesendet)")
|
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]
|
del last_payloads[gid]
|
||||||
time.sleep(POLL_INTERVAL)
|
time.sleep(POLL_INTERVAL)
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
paho-mqtt
|
|
||||||
dotenv
|
|
||||||
requests
|
|
||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user