environments Adding new migrations for renaming and adding fields to the database schema persistent uuid for simclient
187 lines
5.4 KiB
Python
187 lines
5.4 KiB
Python
# simclient/simclient.py
|
|
|
|
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
|
|
|
|
# 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 = "/tmp/simclient.log"
|
|
log_handlers = [logging.FileHandler(LOG_PATH, 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()}")
|
|
# 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")
|
|
|
|
# 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)
|
|
|
|
# Heartbeat-Phase
|
|
while True:
|
|
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()
|