Complete Redesign of Backend Handling for Client Group Assignments
This commit is contained in:
142
server/mqtt_helper.py
Normal file
142
server/mqtt_helper.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Einfache MQTT-Hilfsfunktion für Client-Gruppenzuordnungen
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def publish_client_group(client_uuid: str, group_id: int) -> bool:
|
||||
"""
|
||||
Publiziert die Gruppenzuordnung eines Clients als retained message
|
||||
|
||||
Args:
|
||||
client_uuid: UUID des Clients
|
||||
group_id: ID der Gruppe
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg, False bei Fehler
|
||||
"""
|
||||
try:
|
||||
# MQTT-Konfiguration aus .env
|
||||
broker_host = os.getenv("MQTT_BROKER_HOST", "mqtt")
|
||||
broker_port = int(os.getenv("MQTT_BROKER_PORT", 1883))
|
||||
username = os.getenv("MQTT_USER")
|
||||
password = os.getenv("MQTT_PASSWORD")
|
||||
|
||||
# Topic und Payload
|
||||
topic = f"infoscreen/{client_uuid}/group_id"
|
||||
payload = json.dumps({
|
||||
"group_id": group_id,
|
||||
"client_uuid": client_uuid
|
||||
})
|
||||
|
||||
# MQTT-Client erstellen und verbinden
|
||||
client = mqtt.Client()
|
||||
if username and password:
|
||||
client.username_pw_set(username, password)
|
||||
|
||||
client.connect(broker_host, broker_port, 60)
|
||||
|
||||
# Retained message publizieren
|
||||
result = client.publish(topic, payload, qos=1, retain=True)
|
||||
result.wait_for_publish(timeout=5.0)
|
||||
|
||||
client.disconnect()
|
||||
|
||||
logger.info(
|
||||
f"Group assignment published for client {client_uuid}: group_id={group_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error publishing group assignment for client {client_uuid}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def publish_multiple_client_groups(client_group_mappings: dict) -> tuple[int, int]:
|
||||
"""
|
||||
Publiziert Gruppenzuordnungen für mehrere Clients in einer Verbindung
|
||||
|
||||
Args:
|
||||
client_group_mappings: Dict mit {client_uuid: group_id}
|
||||
|
||||
Returns:
|
||||
tuple: (success_count, failed_count)
|
||||
"""
|
||||
try:
|
||||
broker_host = os.getenv("MQTT_BROKER_HOST", "mqtt")
|
||||
broker_port = int(os.getenv("MQTT_BROKER_PORT", 1883))
|
||||
username = os.getenv("MQTT_USER")
|
||||
password = os.getenv("MQTT_PASSWORD")
|
||||
|
||||
client = mqtt.Client()
|
||||
if username and password:
|
||||
client.username_pw_set(username, password)
|
||||
|
||||
client.connect(broker_host, broker_port, 60)
|
||||
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for client_uuid, group_id in client_group_mappings.items():
|
||||
try:
|
||||
topic = f"infoscreen/{client_uuid}/group_id"
|
||||
payload = json.dumps({
|
||||
"group_id": group_id,
|
||||
"client_uuid": client_uuid
|
||||
})
|
||||
|
||||
result = client.publish(topic, payload, qos=1, retain=True)
|
||||
result.wait_for_publish(timeout=5.0)
|
||||
success_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish for {client_uuid}: {e}")
|
||||
failed_count += 1
|
||||
|
||||
client.disconnect()
|
||||
|
||||
logger.info(
|
||||
f"Bulk publish completed: {success_count} success, {failed_count} failed")
|
||||
return success_count, failed_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in bulk publish: {e}")
|
||||
return 0, len(client_group_mappings)
|
||||
|
||||
|
||||
def delete_client_group_message(client_uuid: str) -> bool:
|
||||
"""
|
||||
Löscht die retained message für einen Client (bei Client-Löschung)
|
||||
"""
|
||||
try:
|
||||
broker_host = os.getenv("MQTT_BROKER_HOST", "mqtt")
|
||||
broker_port = int(os.getenv("MQTT_BROKER_PORT", 1883))
|
||||
username = os.getenv("MQTT_USER")
|
||||
password = os.getenv("MQTT_PASSWORD")
|
||||
|
||||
topic = f"infoscreen/{client_uuid}/group_id"
|
||||
|
||||
client = mqtt.Client()
|
||||
if username and password:
|
||||
client.username_pw_set(username, password)
|
||||
|
||||
client.connect(broker_host, broker_port, 60)
|
||||
|
||||
# Leere retained message löscht die vorherige
|
||||
result = client.publish(topic, "", qos=1, retain=True)
|
||||
result.wait_for_publish(timeout=5.0)
|
||||
|
||||
client.disconnect()
|
||||
|
||||
logger.info(f"Deleted retained group message for client {client_uuid}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error deleting group message for client {client_uuid}: {e}")
|
||||
return False
|
||||
@@ -1,12 +1,49 @@
|
||||
from server.database import Session
|
||||
from models.models import Client, ClientGroup
|
||||
from flask import Blueprint, request, jsonify
|
||||
from server.mqtt_helper import publish_client_group, delete_client_group_message, publish_multiple_client_groups
|
||||
import sys
|
||||
sys.path.append('/workspace')
|
||||
|
||||
clients_bp = Blueprint("clients", __name__, url_prefix="/api/clients")
|
||||
|
||||
|
||||
@clients_bp.route("/sync-all-groups", methods=["POST"])
|
||||
def sync_all_client_groups():
|
||||
"""
|
||||
Administrative Route: Synchronisiert alle bestehenden Client-Gruppenzuordnungen mit MQTT
|
||||
Nützlich für die einmalige Migration bestehender Clients
|
||||
"""
|
||||
session = Session()
|
||||
try:
|
||||
# Alle aktiven Clients abrufen
|
||||
clients = session.query(Client).filter(Client.is_active == True).all()
|
||||
|
||||
if not clients:
|
||||
session.close()
|
||||
return jsonify({"message": "Keine aktiven Clients gefunden", "synced": 0})
|
||||
|
||||
# Alle Clients synchronisieren
|
||||
client_group_mappings = {
|
||||
client.uuid: client.group_id for client in clients}
|
||||
success_count, failed_count = publish_multiple_client_groups(
|
||||
client_group_mappings)
|
||||
|
||||
session.close()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Synchronisation abgeschlossen",
|
||||
"synced": success_count,
|
||||
"failed": failed_count,
|
||||
"total": len(clients)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
session.close()
|
||||
return jsonify({"error": f"Fehler bei der Synchronisation: {str(e)}"}), 500
|
||||
|
||||
|
||||
@clients_bp.route("/without_description", methods=["GET"])
|
||||
def get_clients_without_description():
|
||||
session = Session()
|
||||
@@ -46,10 +83,20 @@ def set_client_description(uuid):
|
||||
if not client:
|
||||
session.close()
|
||||
return jsonify({"error": "Client nicht gefunden"}), 404
|
||||
|
||||
client.description = description
|
||||
session.commit()
|
||||
|
||||
# MQTT: Gruppenzuordnung publizieren (wichtig für neue Clients aus SetupMode)
|
||||
mqtt_success = publish_client_group(client.uuid, client.group_id)
|
||||
|
||||
session.close()
|
||||
return jsonify({"success": True})
|
||||
|
||||
response = {"success": True}
|
||||
if not mqtt_success:
|
||||
response["warning"] = "Beschreibung gespeichert, aber MQTT-Publishing fehlgeschlagen"
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@clients_bp.route("", methods=["GET"])
|
||||
@@ -83,18 +130,51 @@ def get_clients():
|
||||
def update_clients_group():
|
||||
data = request.get_json()
|
||||
client_ids = data.get("client_ids", [])
|
||||
group_id = data.get("group_id")
|
||||
group_name = data.get("group_name")
|
||||
|
||||
if not isinstance(client_ids, list) or len(client_ids) == 0:
|
||||
return jsonify({"error": "client_ids muss eine nicht-leere Liste sein"}), 400
|
||||
|
||||
session = Session()
|
||||
group = session.query(ClientGroup).filter_by(name=group_name).first()
|
||||
if not group:
|
||||
|
||||
# Bestimme Ziel-Gruppe: Priorität hat group_id, ansonsten group_name
|
||||
group = None
|
||||
if group_id is not None:
|
||||
group = session.query(ClientGroup).filter_by(id=group_id).first()
|
||||
if not group:
|
||||
session.close()
|
||||
return jsonify({"error": f"Gruppe mit id={group_id} nicht gefunden"}), 404
|
||||
elif group_name:
|
||||
group = session.query(ClientGroup).filter_by(name=group_name).first()
|
||||
if not group:
|
||||
session.close()
|
||||
return jsonify({"error": f"Gruppe '{group_name}' nicht gefunden"}), 404
|
||||
else:
|
||||
session.close()
|
||||
return jsonify({"error": "Gruppe nicht gefunden"}), 404
|
||||
return jsonify({"error": "Entweder group_id oder group_name ist erforderlich"}), 400
|
||||
|
||||
# WICHTIG: group.id vor dem Schließen puffern, um DetachedInstanceError zu vermeiden
|
||||
target_group_id = group.id
|
||||
|
||||
session.query(Client).filter(Client.uuid.in_(client_ids)).update(
|
||||
{Client.group_id: group.id}, synchronize_session=False
|
||||
{Client.group_id: target_group_id}, synchronize_session=False
|
||||
)
|
||||
session.commit()
|
||||
session.close()
|
||||
return jsonify({"success": True})
|
||||
|
||||
# MQTT: Gruppenzuordnungen für alle betroffenen Clients publizieren (nutzt gecachten target_group_id)
|
||||
client_group_mappings = {
|
||||
client_id: target_group_id for client_id in client_ids}
|
||||
success_count, failed_count = publish_multiple_client_groups(
|
||||
client_group_mappings)
|
||||
|
||||
response = {"success": True}
|
||||
if failed_count > 0:
|
||||
response[
|
||||
"warning"] = f"Gruppenzuordnung gespeichert, aber {failed_count} MQTT-Publishing(s) fehlgeschlagen"
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@clients_bp.route("/<uuid>", methods=["PATCH"])
|
||||
@@ -194,7 +274,16 @@ def delete_client(uuid):
|
||||
if not client:
|
||||
session.close()
|
||||
return jsonify({"error": "Client nicht gefunden"}), 404
|
||||
|
||||
session.delete(client)
|
||||
session.commit()
|
||||
session.close()
|
||||
return jsonify({"success": True})
|
||||
|
||||
# MQTT: Retained message für gelöschten Client entfernen
|
||||
mqtt_success = delete_client_group_message(uuid)
|
||||
|
||||
response = {"success": True}
|
||||
if not mqtt_success:
|
||||
response["warning"] = "Client gelöscht, aber MQTT-Message-Löschung fehlgeschlagen"
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
54
server/sync_existing_clients.py
Normal file
54
server/sync_existing_clients.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Einmaliges Skript zur Synchronisation aller bestehenden Client-Gruppenzuordnungen
|
||||
Verwendung: python sync_existing_clients.py
|
||||
"""
|
||||
from server.mqtt_helper import publish_multiple_client_groups
|
||||
from models.models import Client
|
||||
from server.database import Session
|
||||
import sys
|
||||
import os
|
||||
sys.path.append('/workspace')
|
||||
|
||||
|
||||
def main():
|
||||
print("Synchronisiere bestehende Client-Gruppenzuordnungen mit MQTT...")
|
||||
|
||||
session = Session()
|
||||
try:
|
||||
# Alle aktiven Clients abrufen
|
||||
clients = session.query(Client).filter(Client.is_active == True).all()
|
||||
|
||||
if not clients:
|
||||
print("Keine aktiven Clients gefunden.")
|
||||
return
|
||||
|
||||
print(f"Gefunden: {len(clients)} aktive Clients")
|
||||
|
||||
# Mapping erstellen
|
||||
client_group_mappings = {
|
||||
client.uuid: client.group_id for client in clients}
|
||||
|
||||
# Alle auf einmal publizieren
|
||||
success_count, failed_count = publish_multiple_client_groups(
|
||||
client_group_mappings)
|
||||
|
||||
print(f"Synchronisation abgeschlossen:")
|
||||
print(f" Erfolgreich: {success_count}")
|
||||
print(f" Fehlgeschlagen: {failed_count}")
|
||||
print(f" Gesamt: {len(clients)}")
|
||||
|
||||
if failed_count == 0:
|
||||
print("✅ Alle Clients erfolgreich synchronisiert!")
|
||||
else:
|
||||
print(
|
||||
f"⚠️ {failed_count} Clients konnten nicht synchronisiert werden.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler: {e}")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user