Implementiere Uptime-Überwachung für Steckdosen mit Dashboard-Anzeige

This commit is contained in:
root
2025-03-13 08:56:22 +01:00
parent 63b04c4dea
commit 52a336a788
2 changed files with 515 additions and 16 deletions

View File

@@ -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):