Implementiere Uptime-Überwachung für Steckdosen mit Dashboard-Anzeige
This commit is contained in:
255
backend/app.py
255
backend/app.py
@@ -90,7 +90,9 @@ def init_db():
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0,
|
||||
ip_address TEXT
|
||||
ip_address TEXT,
|
||||
last_seen TIMESTAMP,
|
||||
connection_status TEXT DEFAULT 'unknown'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS job (
|
||||
@@ -106,6 +108,15 @@ def init_db():
|
||||
FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS socket_uptime (
|
||||
id TEXT PRIMARY KEY,
|
||||
socket_id TEXT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT NOT NULL,
|
||||
duration_seconds INTEGER,
|
||||
FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE
|
||||
);
|
||||
''')
|
||||
|
||||
db.commit()
|
||||
@@ -356,11 +367,44 @@ def socket_to_dict(socket):
|
||||
latest_job = get_latest_job_for_socket(socket['id'])
|
||||
waiting_jobs = get_waiting_jobs_for_socket(socket['id'])
|
||||
|
||||
# Verbindungsstatus-Informationen
|
||||
connection_status = socket.get('connection_status', 'unknown')
|
||||
last_seen = socket.get('last_seen')
|
||||
uptime_info = None
|
||||
|
||||
if last_seen and connection_status == 'offline':
|
||||
# Berechne wie lange die Steckdose offline ist
|
||||
try:
|
||||
last_seen_dt = datetime.datetime.fromisoformat(last_seen)
|
||||
now = datetime.datetime.utcnow()
|
||||
offline_duration = int((now - last_seen_dt).total_seconds())
|
||||
|
||||
# Formatiere die Offline-Zeit benutzerfreundlich
|
||||
hours, remainder = divmod(offline_duration, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
uptime_info = {
|
||||
'offline_since': last_seen,
|
||||
'offline_duration': offline_duration,
|
||||
'offline_duration_formatted': f"{hours}h {minutes}m {seconds}s"
|
||||
}
|
||||
except (ValueError, TypeError):
|
||||
# Wenn das Datum nicht geparst werden kann
|
||||
uptime_info = {
|
||||
'offline_since': last_seen,
|
||||
'offline_duration': None,
|
||||
'offline_duration_formatted': "Unbekannt"
|
||||
}
|
||||
|
||||
return {
|
||||
'id': socket['id'],
|
||||
'name': socket['name'],
|
||||
'description': socket['description'],
|
||||
'status': socket['status'],
|
||||
'ipAddress': socket.get('ip_address'),
|
||||
'connectionStatus': connection_status,
|
||||
'lastSeen': last_seen,
|
||||
'uptimeInfo': uptime_info,
|
||||
'latestJob': job_to_dict(latest_job) if latest_job else None,
|
||||
'waitingJobs': [job_to_dict(job) for job in waiting_jobs] if waiting_jobs else []
|
||||
}
|
||||
@@ -510,6 +554,87 @@ def job_to_dict(job):
|
||||
'remainingMinutes': calculate_remaining_time(job)
|
||||
}
|
||||
|
||||
# Socket Uptime-Überwachung
|
||||
def log_socket_connection_event(socket_id, status, duration_seconds=None):
|
||||
"""Speichert ein Ereignis zum Verbindungsstatus einer Steckdose"""
|
||||
event_id = str(uuid.uuid4())
|
||||
timestamp = datetime.datetime.utcnow().isoformat()
|
||||
|
||||
db = get_db()
|
||||
db.execute(
|
||||
'INSERT INTO socket_uptime (id, socket_id, timestamp, status, duration_seconds) VALUES (?, ?, ?, ?, ?)',
|
||||
(event_id, socket_id, timestamp, status, duration_seconds)
|
||||
)
|
||||
db.commit()
|
||||
app.logger.info(f"Verbindungsstatus für Steckdose {socket_id} geändert: {status}")
|
||||
|
||||
# Aktualisiere auch den Verbindungsstatus in der socket-Tabelle
|
||||
db.execute(
|
||||
'UPDATE socket SET connection_status = ?, last_seen = ? WHERE id = ?',
|
||||
(status, timestamp if status == 'online' else None, socket_id)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return event_id
|
||||
|
||||
def get_socket_uptime_events(socket_id=None, limit=100):
|
||||
"""Ruft Verbindungsereignisse für eine oder alle Steckdosen ab"""
|
||||
db = get_db()
|
||||
|
||||
if socket_id:
|
||||
rows = db.execute('''
|
||||
SELECT su.*, s.name, s.ip_address FROM socket_uptime su
|
||||
JOIN socket s ON su.socket_id = s.id
|
||||
WHERE su.socket_id = ?
|
||||
ORDER BY su.timestamp DESC
|
||||
LIMIT ?
|
||||
''', (socket_id, limit)).fetchall()
|
||||
else:
|
||||
rows = db.execute('''
|
||||
SELECT su.*, s.name, s.ip_address FROM socket_uptime su
|
||||
JOIN socket s ON su.socket_id = s.id
|
||||
ORDER BY su.timestamp DESC
|
||||
LIMIT ?
|
||||
''', (limit,)).fetchall()
|
||||
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def check_socket_connection(socket_id):
|
||||
"""Überprüft die Verbindung zu einer Steckdose und aktualisiert den Status"""
|
||||
socket = get_socket_by_id(socket_id)
|
||||
if not socket or not socket['ip_address']:
|
||||
return False
|
||||
|
||||
previous_status = socket.get('connection_status', 'unknown')
|
||||
last_seen = socket.get('last_seen')
|
||||
|
||||
try:
|
||||
device = get_socket_device(socket['ip_address'])
|
||||
if device:
|
||||
# Verbindung erfolgreich
|
||||
if previous_status != 'online':
|
||||
# Status hat sich von offline/unknown auf online geändert
|
||||
duration = None
|
||||
if previous_status == 'offline' and last_seen:
|
||||
# Berechne die Dauer des Ausfalls
|
||||
offline_since = datetime.datetime.fromisoformat(last_seen)
|
||||
now = datetime.datetime.utcnow()
|
||||
duration = int((now - offline_since).total_seconds())
|
||||
|
||||
log_socket_connection_event(socket_id, 'online', duration)
|
||||
return True
|
||||
else:
|
||||
# Keine Verbindung möglich
|
||||
if previous_status != 'offline':
|
||||
# Status hat sich von online/unknown auf offline geändert
|
||||
log_socket_connection_event(socket_id, 'offline')
|
||||
return False
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler bei der Überprüfung der Steckdose {socket['ip_address']}: {e}")
|
||||
if previous_status != 'offline':
|
||||
log_socket_connection_event(socket_id, 'offline')
|
||||
return False
|
||||
|
||||
# Steckdosen-Steuerung mit PyP100
|
||||
def get_socket_device(ip_address):
|
||||
try:
|
||||
@@ -1072,6 +1197,11 @@ def stats():
|
||||
total_sockets = db.execute('SELECT COUNT(*) as count FROM socket').fetchone()['count']
|
||||
available_sockets = db.execute('SELECT COUNT(*) as count FROM socket WHERE status = 0').fetchone()['count']
|
||||
|
||||
# Verbindungsstatistiken
|
||||
online_sockets = db.execute("SELECT COUNT(*) as count FROM socket WHERE connection_status = 'online'").fetchone()['count']
|
||||
offline_sockets = db.execute("SELECT COUNT(*) as count FROM socket WHERE connection_status = 'offline'").fetchone()['count']
|
||||
unknown_sockets = db.execute("SELECT COUNT(*) as count FROM socket WHERE connection_status = 'unknown' OR connection_status IS NULL").fetchone()['count']
|
||||
|
||||
# Job-Statistiken
|
||||
total_jobs = db.execute('SELECT COUNT(*) as count FROM job').fetchone()['count']
|
||||
|
||||
@@ -1095,11 +1225,30 @@ def stats():
|
||||
avg_duration_result = db.execute('SELECT AVG(duration_in_minutes) as avg FROM job').fetchone()
|
||||
avg_duration = int(avg_duration_result['avg']) if avg_duration_result['avg'] else 0
|
||||
|
||||
# Steckdosen-Fehlerstatistiken (letzten 7 Tage)
|
||||
seven_days_ago = (datetime.datetime.utcnow() - timedelta(days=7)).isoformat()
|
||||
outages = db.execute('''
|
||||
SELECT COUNT(*) as count FROM socket_uptime
|
||||
WHERE status = 'offline'
|
||||
AND timestamp > ?
|
||||
''', (seven_days_ago,)).fetchone()['count']
|
||||
|
||||
# Steckdosen mit aktuellen Problemen
|
||||
problem_sockets = db.execute('''
|
||||
SELECT s.name, s.connection_status, s.last_seen
|
||||
FROM socket s
|
||||
WHERE s.connection_status = 'offline'
|
||||
''').fetchall()
|
||||
|
||||
return jsonify({
|
||||
'printers': {
|
||||
'total': total_sockets,
|
||||
'available': available_sockets,
|
||||
'utilization_rate': (total_sockets - available_sockets) / total_sockets if total_sockets > 0 else 0
|
||||
'utilization_rate': (total_sockets - available_sockets) / total_sockets if total_sockets > 0 else 0,
|
||||
'online': online_sockets,
|
||||
'offline': offline_sockets,
|
||||
'unknown': unknown_sockets,
|
||||
'connectivity_rate': online_sockets / total_sockets if total_sockets > 0 else 0
|
||||
},
|
||||
'jobs': {
|
||||
'total': total_jobs,
|
||||
@@ -1109,9 +1258,62 @@ def stats():
|
||||
},
|
||||
'users': {
|
||||
'total': total_users
|
||||
},
|
||||
'uptime': {
|
||||
'outages_last_7_days': outages,
|
||||
'problem_printers': [{'name': row['name'], 'status': row['connection_status'], 'last_seen': row['last_seen']} for row in problem_sockets]
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/api/uptime', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def uptime_stats():
|
||||
"""Liefert detaillierte Uptime-Statistiken für das Dashboard."""
|
||||
socket_id = request.args.get('socket_id')
|
||||
limit = int(request.args.get('limit', 100))
|
||||
|
||||
# Rufe die letzten Uptime-Ereignisse ab
|
||||
events = get_socket_uptime_events(socket_id, limit)
|
||||
|
||||
# Gruppiere Ereignisse nach Steckdose
|
||||
sockets = {}
|
||||
for event in events:
|
||||
socket_id = event['socket_id']
|
||||
if socket_id not in sockets:
|
||||
sockets[socket_id] = {
|
||||
'id': socket_id,
|
||||
'name': event['name'],
|
||||
'ip_address': event['ip_address'],
|
||||
'events': []
|
||||
}
|
||||
|
||||
# Füge Ereignis zur Steckdosenliste hinzu
|
||||
sockets[socket_id]['events'].append({
|
||||
'id': event['id'],
|
||||
'timestamp': event['timestamp'],
|
||||
'status': event['status'],
|
||||
'duration_seconds': event['duration_seconds']
|
||||
})
|
||||
|
||||
# Hole den aktuellen Status aller Steckdosen
|
||||
all_sockets = get_all_sockets()
|
||||
current_status = {}
|
||||
for socket in all_sockets:
|
||||
current_status[socket['id']] = {
|
||||
'connection_status': socket.get('connection_status', 'unknown'),
|
||||
'last_seen': socket.get('last_seen')
|
||||
}
|
||||
|
||||
# Füge den aktuellen Status zu den Socket-Informationen hinzu
|
||||
for socket_id, socket_data in sockets.items():
|
||||
if socket_id in current_status:
|
||||
socket_data['current_status'] = current_status[socket_id]
|
||||
|
||||
return jsonify({
|
||||
'sockets': list(sockets.values())
|
||||
})
|
||||
|
||||
# Regelmäßige Überprüfung der Jobs und automatische Abschaltung der Steckdosen
|
||||
def check_jobs():
|
||||
"""Überprüft abgelaufene Jobs und schaltet Steckdosen automatisch aus."""
|
||||
@@ -1147,24 +1349,63 @@ def check_jobs():
|
||||
|
||||
app.logger.info(f"{len(expired_jobs)} abgelaufene Jobs überprüft, {handled_jobs} Steckdosen aktualisiert.")
|
||||
|
||||
# Hintergrund-Thread für das Job-Polling
|
||||
def check_socket_connections():
|
||||
"""Überprüft periodisch die Verbindung zu allen Steckdosen."""
|
||||
with app.app_context():
|
||||
sockets = get_all_sockets()
|
||||
app.logger.info(f"Überprüfe Verbindungsstatus von {len(sockets)} Steckdosen")
|
||||
|
||||
online_count = 0
|
||||
offline_count = 0
|
||||
|
||||
for socket in sockets:
|
||||
if not socket['ip_address']:
|
||||
continue # Überspringe Steckdosen ohne IP-Adresse
|
||||
|
||||
is_online = check_socket_connection(socket['id'])
|
||||
if is_online:
|
||||
online_count += 1
|
||||
else:
|
||||
offline_count += 1
|
||||
|
||||
app.logger.info(f"Verbindungsüberprüfung abgeschlossen: {online_count} online, {offline_count} offline")
|
||||
|
||||
# Hintergrund-Thread für das Job-Polling und Steckdosen-Monitoring
|
||||
def background_job_checker():
|
||||
"""Hintergrund-Thread, der regelmäßig abgelaufene Jobs überprüft."""
|
||||
app.logger.info("Starte Hintergrund-Thread für Job-Überprüfung")
|
||||
"""Hintergrund-Thread, der regelmäßig abgelaufene Jobs und Steckdosenverbindungen überprüft."""
|
||||
app.logger.info("Starte Hintergrund-Thread für Job-Überprüfung und Steckdosen-Monitoring")
|
||||
|
||||
# Standardintervall für Socket-Überprüfungen (5 Minuten)
|
||||
socket_check_interval = int(os.environ.get('SOCKET_CHECK_INTERVAL', '300'))
|
||||
last_socket_check = 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Überprüfe Jobs bei jedem Durchlauf
|
||||
check_jobs()
|
||||
|
||||
# Überprüfe Steckdosen nur in längeren Intervallen
|
||||
current_time = time.time()
|
||||
if current_time - last_socket_check >= socket_check_interval:
|
||||
check_socket_connections()
|
||||
last_socket_check = current_time
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Fehler im Hintergrund-Thread für Job-Überprüfung: {e}")
|
||||
app.logger.error(f"Fehler im Hintergrund-Thread: {e}")
|
||||
|
||||
# Pause zwischen den Überprüfungen
|
||||
time.sleep(app.config['JOB_CHECK_INTERVAL'])
|
||||
|
||||
# CLI-Befehl für manuelle Ausführung
|
||||
# CLI-Befehle für manuelle Ausführung
|
||||
@app.cli.command("check-jobs")
|
||||
def cli_check_jobs():
|
||||
"""CLI-Befehl zur manuellen Überprüfung abgelaufener Jobs."""
|
||||
check_jobs()
|
||||
|
||||
@app.cli.command("check-sockets")
|
||||
def cli_check_sockets():
|
||||
"""CLI-Befehl zur manuellen Überprüfung aller Steckdosenverbindungen."""
|
||||
check_socket_connections()
|
||||
|
||||
@app.route('/api/job/<job_id>/status', methods=['GET'])
|
||||
def job_status(job_id):
|
||||
|
Reference in New Issue
Block a user